From 7caec8a3f984c0e5f40e98d702951376f56eab81 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 21 Apr 2026 15:01:18 +0300 Subject: [PATCH] net.http: fix post requests not working (fixes #24226) --- vlib/net/http/backend.c.v | 10 +-- vlib/net/http/backend_vschannel_windows.c.v | 4 +- vlib/net/http/http_proxy.v | 10 +-- vlib/net/http/http_proxy_test.v | 2 +- vlib/net/http/request.v | 81 +++++++++++++++---- vlib/net/http/server_test.v | 89 +++++++++++++++++++++ 6 files changed, 166 insertions(+), 30 deletions(-) diff --git a/vlib/net/http/backend.c.v b/vlib/net/http/backend.c.v index ced13a836..c9872dd22 100644 --- a/vlib/net/http/backend.c.v +++ b/vlib/net/http/backend.c.v @@ -6,20 +6,20 @@ module http import net.ssl import strings -fn (req &Request) ssl_do(port int, method Method, host_name string, path string) !Response { +fn (req &Request) ssl_do(port int, method Method, host_name string, path string, data string, header Header) !Response { $if windows && !no_vschannel ? { if req.validate { - return vschannel_ssl_do(req, port, method, host_name, path) + return vschannel_ssl_do(req, port, method, host_name, path, data, header) } // vschannel enforces certificate validation during handshake. // Use net.ssl when validation is explicitly disabled. } - return net_ssl_do(req, port, method, host_name, path) + return net_ssl_do(req, port, method, host_name, path, data, header) } -fn net_ssl_do(req &Request, port int, method Method, host_name string, path string) !Response { +fn net_ssl_do(req &Request, port int, method Method, host_name string, path string, data string, header Header) !Response { mut retries := 0 - req_headers := req.build_request_headers(method, host_name, port, path) + req_headers := req.build_request_headers_with(method, host_name, port, path, data, header) $if trace_http_request ? { eprint('> ') eprint(req_headers) diff --git a/vlib/net/http/backend_vschannel_windows.c.v b/vlib/net/http/backend_vschannel_windows.c.v index 179fbeb19..f4eb34397 100644 --- a/vlib/net/http/backend_vschannel_windows.c.v +++ b/vlib/net/http/backend_vschannel_windows.c.v @@ -14,12 +14,12 @@ const C.vsc_init_resp_buff_size int fn C.new_tls_context() C.TlsContext fn C.vschannel_last_error(tls_ctx &C.TlsContext) int -fn vschannel_ssl_do(req &Request, port int, method Method, host_name string, path string) !Response { +fn vschannel_ssl_do(req &Request, port int, method Method, host_name string, path string, data string, header Header) !Response { mut ctx := C.new_tls_context() C.vschannel_init(&ctx) mut buff := unsafe { malloc_noscan(C.vsc_init_resp_buff_size) } addr := host_name - sdata := req.build_request_headers(method, host_name, port, path) + sdata := req.build_request_headers_with(method, host_name, port, path, data, header) $if trace_http_request ? { eprintln('> ${sdata}') } diff --git a/vlib/net/http/http_proxy.v b/vlib/net/http/http_proxy.v index 4c4b6d8ea..8a6733555 100644 --- a/vlib/net/http/http_proxy.v +++ b/vlib/net/http/http_proxy.v @@ -139,7 +139,7 @@ fn (pr &HttpProxy) connect_tcp(host string) !&net.TcpConn { } } -fn (pr &HttpProxy) http_do(host urllib.URL, _method Method, path string, req &Request) !Response { +fn (pr &HttpProxy) http_do(host urllib.URL, method Method, path string, req &Request, data string, header Header) !Response { host_name := host.hostname() mut port := host.port().int() if port == 0 { @@ -151,8 +151,8 @@ fn (pr &HttpProxy) http_do(host urllib.URL, _method Method, path string, req &Re ':${port}' } - s := req.build_request_headers(req.method, host_name, port, - '${host.scheme}://${host_name}${port_part}${path}') + s := req.build_request_headers_with(method, host_name, port, + '${host.scheme}://${host_name}${port_part}${path}', data, header) if host.scheme == 'https' { mut client := pr.ssl_dial('${host_name}:${port}')! @@ -163,8 +163,8 @@ fn (pr &HttpProxy) http_do(host urllib.URL, _method Method, path string, req &Re // client.shutdown()! // return response_text } $else { - return req.do_request(req.build_request_headers(req.method, host_name, port, path), mut - client)! + return req.do_request(req.build_request_headers_with(method, host_name, port, path, + data, header), mut client)! } } else if host.scheme == 'http' { mut client := pr.dial('${host_name}:${port}')! diff --git a/vlib/net/http/http_proxy_test.v b/vlib/net/http/http_proxy_test.v index 5b55c3c6e..0016467cf 100644 --- a/vlib/net/http/http_proxy_test.v +++ b/vlib/net/http/http_proxy_test.v @@ -235,7 +235,7 @@ fn test_http_proxy_do() { res := proxy.http_do(urllib.parse('http://httpbin.org/headers')!, Method.get, '/headers', &Request{ proxy: proxy header: header - })! + }, '', header)! println(res.status_code) println('he4aders ${res.header}') assert res.status_code == 200 diff --git a/vlib/net/http/request.v b/vlib/net/http/request.v index 93c7bd006..c5dcdc8cc 100644 --- a/vlib/net/http/request.v +++ b/vlib/net/http/request.v @@ -164,17 +164,21 @@ pub fn (req &Request) do() !Response { mut url := urllib.parse(req.url) or { return error('http.Request.do: invalid url ${req.url}') } mut rurl := url mut resp := Response{} + mut method := req.method + mut data := req.data + mut header := req.header mut nredirects := 0 for { if nredirects == max_redirects { return error('http.request.do: maximum number of redirects reached (${max_redirects})') } - qresp := req.method_and_url_to_response(req.method, rurl)! + qresp := req.method_and_url_to_response(method, rurl, data, header)! resp = qresp if !req.allow_redirect { break } - if resp.status() !in [.moved_permanently, .found, .see_other, .temporary_redirect, + status := resp.status() + if status !in [.moved_permanently, .found, .see_other, .temporary_redirect, .permanent_redirect] { break } @@ -192,13 +196,48 @@ pub fn (req &Request) do() !Response { qrurl := urllib.parse(redirect_url) or { return error('http.request.do: invalid URL in redirect "${redirect_url}"') } + method, data, header = redirected_request_parts(method, status, data, header) rurl = qrurl nredirects++ } return resp } -fn (req &Request) method_and_url_to_response(method Method, url urllib.URL) !Response { +fn redirected_request_parts(method Method, status Status, data string, header Header) (Method, string, Header) { + next_method := redirected_method(method, status) + if next_method == method { + return method, data, header + } + mut next_header := header + next_header.delete(.content_length) + next_header.delete(.content_type) + next_header.delete(.transfer_encoding) + return next_method, '', next_header +} + +fn redirected_method(method Method, status Status) Method { + return match status { + .see_other { + if method == Method.head { + Method.head + } else { + Method.get + } + } + .moved_permanently, .found { + if method == Method.post { + Method.get + } else { + method + } + } + else { + method + } + } +} + +fn (req &Request) method_and_url_to_response(method Method, url urllib.URL, data string, header Header) !Response { host_name := url.hostname() scheme := url.scheme p := url.escaped_path().trim_left('/') @@ -216,7 +255,7 @@ fn (req &Request) method_and_url_to_response(method Method, url urllib.URL) !Res if scheme == 'https' && req.proxy == unsafe { nil } { // println('ssl_do( ${nport}, ${method}, ${host_name}, ${path} )') for i in 0 .. req.max_retries { - res := req.ssl_do(nport, method, host_name, path) or { + res := req.ssl_do(nport, method, host_name, path, data, header) or { if i == req.max_retries - 1 || is_no_need_retry_error(err.code()) { return err } @@ -227,7 +266,7 @@ fn (req &Request) method_and_url_to_response(method Method, url urllib.URL) !Res } else if scheme == 'http' && req.proxy == unsafe { nil } { // println('http_do( ${nport}, ${method}, ${host_name}, ${path} )') for i in 0 .. req.max_retries { - res := req.http_do('${host_name}:${nport}', method, path) or { + res := req.http_do('${host_name}:${nport}', method, path, data, header) or { if i == req.max_retries - 1 || is_no_need_retry_error(err.code()) { return err } @@ -237,7 +276,7 @@ fn (req &Request) method_and_url_to_response(method Method, url urllib.URL) !Res } } else if req.proxy != unsafe { nil } { for i in 0 .. req.max_retries { - res := req.proxy.http_do(url, method, path, req) or { + res := req.proxy.http_do(url, method, path, req, data, header) or { if i == req.max_retries - 1 || is_no_need_retry_error(err.code()) { return err } @@ -250,6 +289,10 @@ fn (req &Request) method_and_url_to_response(method Method, url urllib.URL) !Res } fn (req &Request) build_request_headers(method Method, host_name string, port int, path string) string { + return req.build_request_headers_with(method, host_name, port, path, req.data, req.header) +} + +fn (req &Request) build_request_headers_with(method Method, host_name string, port int, path string, data string, header Header) string { mut sb := strings.new_builder(4096) version := if req.version == .unknown { Version.v1_1 } else { req.version } sb.write_string(method.str()) @@ -258,7 +301,7 @@ fn (req &Request) build_request_headers(method Method, host_name string, port in sb.write_string(' ') sb.write_string(version.str()) sb.write_string('\r\n') - if !req.header.contains(.host) { + if !header.contains(.host) { sb.write_string('Host: ') if port != 80 && port != 443 && port != 0 { sb.write_string('${host_name}:${port}') @@ -267,43 +310,47 @@ fn (req &Request) build_request_headers(method Method, host_name string, port in } sb.write_string('\r\n') } - if !req.header.contains(.user_agent) { + if !header.contains(.user_agent) { ua := req.user_agent sb.write_string('User-Agent: ') sb.write_string(ua) sb.write_string('\r\n') } - if !req.header.contains(.content_length) { + if !header.contains(.content_length) { // Write Content-Length: 0 even if there's no content, since some APIs // stop working without this header. sb.write_string('Content-Length: ') - sb.write_string(req.data.len.str()) + sb.write_string(data.len.str()) sb.write_string('\r\n') } chkey := CommonHeader.cookie.str() - for key in req.header.keys() { + for key in header.keys() { if key == chkey { continue } - val := req.header.custom_values(key).join('; ') + val := header.custom_values(key).join('; ') sb.write_string(key) sb.write_string(': ') sb.write_string(val) sb.write_string('\r\n') } - sb.write_string(req.build_request_cookies_header()) + sb.write_string(req.build_request_cookies_header_with_header(header)) sb.write_string('Connection: close\r\n') sb.write_string('\r\n') - sb.write_string(req.data) + sb.write_string(data) return sb.str() } fn (req &Request) build_request_cookies_header() string { + return req.build_request_cookies_header_with_header(req.header) +} + +fn (req &Request) build_request_cookies_header_with_header(header Header) string { if req.cookies.len < 1 { return '' } mut sb_cookie := strings.new_builder(1024) - hvcookies := req.header.values(.cookie) + hvcookies := header.values(.cookie) total_cookies := req.cookies.len + hvcookies.len sb_cookie.write_string('Cookie: ') mut idx := 0 @@ -327,9 +374,9 @@ fn (req &Request) build_request_cookies_header() string { return sb_cookie.str() } -fn (req &Request) http_do(host string, method Method, path string) !Response { +fn (req &Request) http_do(host string, method Method, path string, data string, header Header) !Response { host_name, port := net.split_address(host)! - s := req.build_request_headers(method, host_name, port, path) + s := req.build_request_headers_with(method, host_name, port, path, data, header) mut client := net.dial_tcp(host)! client.set_read_timeout(req.read_timeout) client.set_write_timeout(req.write_timeout) diff --git a/vlib/net/http/server_test.v b/vlib/net/http/server_test.v index e59ecbc3c..aec1bf819 100644 --- a/vlib/net/http/server_test.v +++ b/vlib/net/http/server_test.v @@ -387,6 +387,95 @@ fn test_server_coerces_invalid_status_code_to_internal_server_error() { // +struct RedirectMethodHandler {} + +fn (mut handler RedirectMethodHandler) handle(req http.Request) http.Response { + mut r := http.Response{} + match req.url { + '/redirect-301' { + r.header = http.new_header(key: .location, value: '/expect-get') + r.set_status(.moved_permanently) + } + '/redirect-302' { + r.header = http.new_header(key: .location, value: '/expect-get') + r.set_status(.found) + } + '/redirect-303' { + r.header = http.new_header(key: .location, value: '/expect-get') + r.set_status(.see_other) + } + '/redirect-307' { + r.header = http.new_header(key: .location, value: '/expect-post') + r.set_status(.temporary_redirect) + } + '/redirect-308' { + r.header = http.new_header(key: .location, value: '/expect-post') + r.set_status(.permanent_redirect) + } + '/expect-get' { + if req.method == .get && req.data == '' { + r.body = 'redirected-as-get' + r.set_status(.ok) + } else { + r.body = 'expected GET without a body, got ${req.method} `${req.data}`' + r.set_status(.method_not_allowed) + } + } + '/expect-post' { + if req.method == .post && req.data == 'payload' { + r.body = 'preserved-post' + r.set_status(.ok) + } else { + r.body = 'expected POST with payload, got ${req.method} `${req.data}`' + r.set_status(.method_not_allowed) + } + } + else { + r.set_status(.not_found) + } + } + + r.set_version(req.version) + return r +} + +fn test_redirects_change_post_to_get_only_when_required() { + log.warn('${@FN} started') + defer { log.warn('${@FN} finished') } + mut server := &http.Server{ + accept_timeout: atimeout + handler: RedirectMethodHandler{} + addr: '127.0.0.1:18204' + show_startup_message: false + } + t := spawn server.listen_and_serve() + server.wait_till_running() or { + estr := err.str() + if estr == 'maximum retries reached' { + log.error('>>>> Skipping test ${@FN} since its server could not start, err: ${err}') + return + } + log.fatal(estr) + } + + for path in ['/redirect-301', '/redirect-302', '/redirect-303'] { + resp := http.post('http://${server.addr}${path}', 'payload')! + assert resp.status() == .ok + assert resp.body == 'redirected-as-get' + } + + for path in ['/redirect-307', '/redirect-308'] { + resp := http.post('http://${server.addr}${path}', 'payload')! + assert resp.status() == .ok + assert resp.body == 'preserved-post' + } + + server.stop() + t.wait() +} + +// + struct KeepAliveHandler { mut: request_count int -- 2.39.5