From 2c62c6d0fe270138d8905050d13502c3b5246d8c Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 21 Apr 2026 15:24:45 +0300 Subject: [PATCH] net.http: fix download_file_with_progress in net.http module (fixes #25002) --- vlib/net/http/download_progress.v | 13 ++++++--- vlib/net/http/download_silent_downloader.v | 3 ++ vlib/net/http/request.v | 23 +++++++++++++++ vlib/net/http/request_receive_test.v | 34 ++++++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/vlib/net/http/download_progress.v b/vlib/net/http/download_progress.v index c637a2211..dcb39441a 100644 --- a/vlib/net/http/download_progress.v +++ b/vlib/net/http/download_progress.v @@ -11,8 +11,9 @@ mut: on_start(mut request Request, path string) ! // Called many times, once a chunk of data is received on_chunk(request &Request, chunk []u8, already_received u64, expected u64) ! - // Called once, at the end of the streaming download. Do cleanup here, + // Called once, at the end of the download attempt. Do cleanup here, // like closing a file (opened in on_start), reporting stats etc. + // `response` will be empty when the request fails before a response is parsed. on_finish(request &Request, response &Response) ! } @@ -55,13 +56,17 @@ pub fn download_file_with_progress(url string, path string, params DownloaderPar } mut req := prepare(config)! d.on_start(mut req, path)! - response := req.do()! + mut response := Response{} + defer { + d.on_finish(req, response) or {} + } + response = req.do()! $if windows && !no_vschannel ? { // TODO: remove this, when windows supports streaming properly through vschannel // For now though, just ensure that the complete body is "received" in one big chunk: - d.on_chunk(req, response.body.bytes(), 0, u64(response.body.len))! + body_len := u64(response.body.len) + d.on_chunk(req, response.body.bytes(), body_len, body_len)! } - d.on_finish(req, response)! return response } diff --git a/vlib/net/http/download_silent_downloader.v b/vlib/net/http/download_silent_downloader.v index 74e0837a5..095579d65 100644 --- a/vlib/net/http/download_silent_downloader.v +++ b/vlib/net/http/download_silent_downloader.v @@ -25,4 +25,7 @@ pub fn (mut d SilentStreamingDownloader) on_chunk(request &Request, chunk []u8, // on_finish is called once at the end of the download. pub fn (mut d SilentStreamingDownloader) on_finish(request &Request, response &Response) ! { d.f.close() + if response.status_code == 0 && d.path.len > 0 { + os.rm(d.path) or {} + } } diff --git a/vlib/net/http/request.v b/vlib/net/http/request.v index cecde4327..93c7bd006 100644 --- a/vlib/net/http/request.v +++ b/vlib/net/http/request.v @@ -504,6 +504,15 @@ fn parse_received_response(response_text string, info ReceivedResponseInfo) !Res return parse_response(response_text) } +fn validate_received_response_completion(has_content_length bool, expected_size u64, body_so_far u64, is_chunked_transfer bool, chunked_complete bool) ! { + if has_content_length && body_so_far < expected_size { + return error('http.request: response body ended early: received ${body_so_far} of ${expected_size} bytes') + } + if is_chunked_transfer && !chunked_complete { + return error('http.request: incomplete chunked response') + } +} + fn (req &Request) receive_all_data_from_cb_in_builder(mut content strings.Builder, con voidptr, receive_chunk_cb FnReceiveChunk) !ReceivedResponseInfo { mut buff := [bufsize]u8{} bp := unsafe { &buff[0] } @@ -523,6 +532,13 @@ fn (req &Request) receive_all_data_from_cb_in_builder(mut content strings.Builde readcounter++ len := receive_chunk_cb(con, bp, bufsize) or { if err is io.Eof { + body_so_far := if headers_end >= 0 && old_len > body_pos { + old_len - body_pos + } else { + u64(0) + } + validate_received_response_completion(has_content_length, expected_size, + body_so_far, is_chunked_transfer, chunked_body_tracker.complete)! break } return err @@ -534,6 +550,13 @@ fn (req &Request) receive_all_data_from_cb_in_builder(mut content strings.Builde eprintln('-'.repeat(20)) } if len <= 0 { + body_so_far := if headers_end >= 0 && old_len > body_pos { + old_len - body_pos + } else { + u64(0) + } + validate_received_response_completion(has_content_length, expected_size, body_so_far, + is_chunked_transfer, chunked_body_tracker.complete)! break } new_len = old_len + u64(len) diff --git a/vlib/net/http/request_receive_test.v b/vlib/net/http/request_receive_test.v index d899c6d7b..dc99bb103 100644 --- a/vlib/net/http/request_receive_test.v +++ b/vlib/net/http/request_receive_test.v @@ -97,3 +97,37 @@ fn test_receive_all_data_from_cb_in_builder_dechunks_progress_body_and_parses_tr assert resp.status_code == 200 assert resp.body == '' } + +fn test_receive_all_data_from_cb_in_builder_errors_on_premature_eof_with_content_length() { + mut fixture := ReceiveAllDataFixture{ + parts: [ + 'HTTP/1.1 200 OK\r\nContent-Length: 10\r\nContent-Type: text/plain\r\n\r\nhello', + ] + } + mut req := Request{} + mut content := strings.new_builder(64) + req.receive_all_data_from_cb_in_builder(mut content, voidptr(&fixture), + receive_all_data_fixture_cb) or { + assert err.msg().contains('response body ended early') + assert err.msg().contains('5 of 10 bytes') + return + } + panic('expected an early EOF error for a truncated fixed-length response') +} + +fn test_receive_all_data_from_cb_in_builder_errors_on_incomplete_chunked_response() { + mut fixture := ReceiveAllDataFixture{ + parts: [ + 'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Type: text/plain\r\n\r\n4\r\nWi', + 'ki\r\n6\r\nped', + ] + } + mut req := Request{} + mut content := strings.new_builder(64) + req.receive_all_data_from_cb_in_builder(mut content, voidptr(&fixture), + receive_all_data_fixture_cb) or { + assert err.msg() == 'http.request: incomplete chunked response' + return + } + panic('expected an early EOF error for an incomplete chunked response') +} -- 2.39.5