From d7cac2df6d5a31b72c82807bed0715b9c8b2ba70 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 24 May 2026 15:12:16 +0300 Subject: [PATCH] net.http: make Header.get/get_custom return Option instead of Result (fix #27177) (#27214) --- vlib/mcp/server_test.v | 6 +-- vlib/net/http/header.v | 6 +-- vlib/net/http/header_test.v | 8 +-- vlib/net/http/response_test.v | 6 +-- vlib/veb/context.v | 8 +-- vlib/veb/cors_before_request_test.v | 6 +-- .../content_type_header_regression_test.v | 2 +- vlib/veb/tests/cors_2_test.v | 4 +- vlib/veb/tests/cors_test.v | 4 +- vlib/veb/tests/middleware_test.v | 2 +- vlib/veb/tests/static_compression_test.v | 52 +++++++++---------- vlib/veb/tests/static_handler_test.v | 12 ++--- vlib/veb/tests/veb_test.v | 18 +++---- 13 files changed, 67 insertions(+), 67 deletions(-) diff --git a/vlib/mcp/server_test.v b/vlib/mcp/server_test.v index aae3ffc44..20fcd8ea0 100644 --- a/vlib/mcp/server_test.v +++ b/vlib/mcp/server_test.v @@ -212,7 +212,7 @@ fn test_server_http_sessions_and_delete() { header: list_header )! assert list_response.status_code == 200 - assert list_response.header.get(.content_type)!.starts_with(event_stream_content_type) + assert list_response.header.get(.content_type)?.starts_with(event_stream_content_type) list_messages := parse_sse_messages(list_response.body)! assert list_messages.len == 1 list_result := decode_response(list_messages[0])!.decode_result[ListToolsResult]()! @@ -751,7 +751,7 @@ fn test_http_get_streams_queued_notifications_with_event_ids() { header: get_header )! assert stream.status_code == 200 - assert stream.header.get(.content_type)!.starts_with(event_stream_content_type) + assert stream.header.get(.content_type)?.starts_with(event_stream_content_type) assert stream.body.contains('id: 1') assert stream.body.contains('id: 2') first_messages := parse_sse_messages(stream.body)! @@ -958,7 +958,7 @@ fn test_http_returns_json_when_accept_lists_both() { header: header )! assert ping_response.status_code == 200 - assert ping_response.header.get(.content_type)!.starts_with(default_content_type) + assert ping_response.header.get(.content_type)?.starts_with(default_content_type) server.close() server_thread.wait() or {} diff --git a/vlib/net/http/header.v b/vlib/net/http/header.v index 237acd11e..0f749c684 100644 --- a/vlib/net/http/header.v +++ b/vlib/net/http/header.v @@ -554,13 +554,13 @@ pub fn (h Header) contains_custom(key string, flags HeaderQueryConfig) bool { // get gets the first value for the CommonHeader, or none if the key // does not exist. -pub fn (h Header) get(key CommonHeader) !string { +pub fn (h Header) get(key CommonHeader) ?string { return h.get_custom(key.str()) } // get_custom gets the first value for the custom header, or none if // the key does not exist. -pub fn (h Header) get_custom(key string, flags HeaderQueryConfig) !string { +pub fn (h Header) get_custom(key string, flags HeaderQueryConfig) ?string { if flags.exact { for i := 0; i < h.cur_pos; i++ { // for kv in h.data { @@ -579,7 +579,7 @@ pub fn (h Header) get_custom(key string, flags HeaderQueryConfig) !string { } } } - return error('none') + return none } // starting_with gets the first header starting with key, or none if diff --git a/vlib/net/http/header_test.v b/vlib/net/http/header_test.v index b383040e4..b0ba90149 100644 --- a/vlib/net/http/header_test.v +++ b/vlib/net/http/header_test.v @@ -83,7 +83,7 @@ fn test_delete_header() { mut r := new_request(.get, '', '') r.header.set(.authorization, 'foo') r.header.delete(.authorization) - assert r.header.get(.authorization)! == '' + assert r.header.get(.authorization)? == '' } fn test_custom_header() { @@ -123,9 +123,9 @@ fn test_contains_custom() { fn test_get_custom() { mut h := new_header() h.add_custom('Hello', 'world')! - assert h.get_custom('hello')! == 'world' - assert h.get_custom('HELLO')! == 'world' - assert h.get_custom('Hello', exact: true)! == 'world' + assert h.get_custom('hello')? == 'world' + assert h.get_custom('HELLO')? == 'world' + assert h.get_custom('Hello', exact: true)? == 'world' if _ := h.get_custom('hello', exact: true) { // should be none assert false diff --git a/vlib/net/http/response_test.v b/vlib/net/http/response_test.v index 1e0e5af7e..bc7d4614c 100644 --- a/vlib/net/http/response_test.v +++ b/vlib/net/http/response_test.v @@ -44,7 +44,7 @@ fn test_parse_response() { assert x.status_code == 200 assert x.status_msg == 'OK' assert x.header.contains(.content_length) - assert x.header.get(.content_length)! == '3' + assert x.header.get(.content_length)? == '3' assert x.body == 'Foo' } @@ -56,7 +56,7 @@ fn test_parse_response_with_cookies() { assert x.status_code == 200 assert x.status_msg == 'OK' assert x.header.contains(.content_length) - assert x.header.get(.content_length)! == '3' + assert x.header.get(.content_length)? == '3' assert x.body == 'Foo' response_cookie := x.cookies() assert response_cookie[0].str() == 'id=${cookie_id}' @@ -69,7 +69,7 @@ fn test_parse_response_with_cookies() { assert x.status_code == 200 assert x.status_msg == 'OK' assert x.header.contains(.content_length) - assert x.header.get(.content_length)! == '3' + assert x.header.get(.content_length)? == '3' assert x.body == 'Foo' response_cookie_base64 := x.cookies() assert response_cookie_base64[0].str().split(';')[0] == 'enctoken=${cookie_base64}' diff --git a/vlib/veb/context.v b/vlib/veb/context.v index 4616ea78a..a5ac69108 100644 --- a/vlib/veb/context.v +++ b/vlib/veb/context.v @@ -84,13 +84,13 @@ pub mut: } // returns the request header data from the key -pub fn (ctx &Context) get_header(key http.CommonHeader) !string { - return ctx.req.header.get(key)! +pub fn (ctx &Context) get_header(key http.CommonHeader) ?string { + return ctx.req.header.get(key) } // returns the request header data from the key -pub fn (ctx &Context) get_custom_header(key string) !string { - return ctx.req.header.get_custom(key)! +pub fn (ctx &Context) get_custom_header(key string) ?string { + return ctx.req.header.get_custom(key) } // set a header on the response object diff --git a/vlib/veb/cors_before_request_test.v b/vlib/veb/cors_before_request_test.v index 1517c5093..60486622d 100644 --- a/vlib/veb/cors_before_request_test.v +++ b/vlib/veb/cors_before_request_test.v @@ -74,7 +74,7 @@ fn test_before_request_still_runs_with_cors_middleware() { assert ctx.res.status_code == int(http.Status.bad_request) assert ctx.res.body == 'Authorization failed' - assert ctx.res.header.get_custom('X-BEFORE-REQUEST')! == 'true' + assert ctx.res.header.get_custom('X-BEFORE-REQUEST')? == 'true' } fn test_before_request_can_allow_cors_request() { @@ -87,6 +87,6 @@ fn test_before_request_can_allow_cors_request() { assert ctx.res.status_code == int(http.Status.ok) assert ctx.res.body == 'authorized' - assert ctx.res.header.get_custom('X-BEFORE-REQUEST')! == 'true' - assert ctx.res.header.get(.access_control_allow_origin)! == issue_20757_allowed_origin + assert ctx.res.header.get_custom('X-BEFORE-REQUEST')? == 'true' + assert ctx.res.header.get(.access_control_allow_origin)? == issue_20757_allowed_origin } diff --git a/vlib/veb/tests/content_type_header_regression_test.v b/vlib/veb/tests/content_type_header_regression_test.v index 24297c341..9c59ad524 100644 --- a/vlib/veb/tests/content_type_header_regression_test.v +++ b/vlib/veb/tests/content_type_header_regression_test.v @@ -9,5 +9,5 @@ fn test_set_header_content_type_preserves_charset_parameter() { ctx.text('Hello, World!') - assert ctx.res.header.get(.content_type)! == 'text/html; charset=utf-8' + assert ctx.res.header.get(.content_type)? == 'text/html; charset=utf-8' } diff --git a/vlib/veb/tests/cors_2_test.v b/vlib/veb/tests/cors_2_test.v index 9bdb6f2cd..2a2bcdf65 100644 --- a/vlib/veb/tests/cors_2_test.v +++ b/vlib/veb/tests/cors_2_test.v @@ -76,11 +76,11 @@ fn test_preflight() { assert x.status() == .ok assert x.body == 'ok' - assert x.header.get(.access_control_allow_origin)! == allowed_origin + assert x.header.get(.access_control_allow_origin)? == allowed_origin if _ := x.header.get(.access_control_allow_credentials) { assert false, 'Access-Control-Allow-Credentials should not be present the value is `false`' } - assert x.header.get(.access_control_allow_methods)! == 'GET, HEAD' + assert x.header.get(.access_control_allow_methods)? == 'GET, HEAD' } fn test_invalid_origin() { diff --git a/vlib/veb/tests/cors_test.v b/vlib/veb/tests/cors_test.v index 2e879d72a..f0fe56b7c 100644 --- a/vlib/veb/tests/cors_test.v +++ b/vlib/veb/tests/cors_test.v @@ -76,11 +76,11 @@ fn test_preflight() { assert x.status() == .ok assert x.body == 'ok' - assert x.header.get(.access_control_allow_origin)! == allowed_origin + assert x.header.get(.access_control_allow_origin)? == allowed_origin if _ := x.header.get(.access_control_allow_credentials) { assert false, 'Access-Control-Allow-Credentials should not be present the value is `false`' } - assert x.header.get(.access_control_allow_methods)! == 'GET, HEAD' + assert x.header.get(.access_control_allow_methods)? == 'GET, HEAD' } fn test_invalid_origin() { diff --git a/vlib/veb/tests/middleware_test.v b/vlib/veb/tests/middleware_test.v index c794189d7..531dedb2f 100644 --- a/vlib/veb/tests/middleware_test.v +++ b/vlib/veb/tests/middleware_test.v @@ -224,7 +224,7 @@ fn test_bound_before_request_named_middleware_does_not_timeout() { write_timeout: 1500 * time.millisecond })! assert x.body == 'from index, 2' - assert x.header.get_custom('X-BEFORE-REQUEST-MIDDLEWARE')! == 'true' + assert x.header.get_custom('X-BEFORE-REQUEST-MIDDLEWARE')? == 'true' } fn test_unreachable_order() { diff --git a/vlib/veb/tests/static_compression_test.v b/vlib/veb/tests/static_compression_test.v index 44a20f339..3f511b396 100644 --- a/vlib/veb/tests/static_compression_test.v +++ b/vlib/veb/tests/static_compression_test.v @@ -228,8 +228,8 @@ fn test_gzip_compression_with_accept_encoding() { x := req.do()! assert x.status() == .ok - assert x.header.get(.content_encoding)! == 'gzip' - assert x.header.get(.vary)! == 'Accept-Encoding' + assert x.header.get(.content_encoding)? == 'gzip' + assert x.header.get(.vary)? == 'Accept-Encoding' // HTTP client auto-decompresses gzip, so verify the content directly assert x.body == test_file_content @@ -264,11 +264,11 @@ fn test_gz_file_cache_creation() { // Second request should use cached .gz file y := req.do()! assert y.status() == .ok - assert y.header.get(.content_encoding)! == 'gzip' + assert y.header.get(.content_encoding)? == 'gzip' // Verify Content-Length matches .gz file size (tests os.file_size() code path) gz_file_size := os.file_size(gz_path) - content_length := y.header.get(.content_length)!.u64() + content_length := y.header.get(.content_length)?.u64() assert content_length == gz_file_size, 'Content-Length should match .gz file size' } @@ -284,7 +284,7 @@ fn test_large_file_not_auto_compressed() { assert x.status() == .ok // File should be compressed as it's under 1MB threshold - assert x.header.get(.content_encoding)! == 'gzip' + assert x.header.get(.content_encoding)? == 'gzip' } fn test_already_compressed_flag() { @@ -322,7 +322,7 @@ fn test_readonly_filesystem_fallback() { assert x.status() == .ok // Should still be compressed. - assert x.header.get(.content_encoding)! == 'gzip' + assert x.header.get(.content_encoding)? == 'gzip' // Verify that .gz file was NOT created beside the source file. gz_path := '${readonly_file}.gz' @@ -355,7 +355,7 @@ fn test_readonly_filesystem_fallback_zstd() { assert x.status() == .ok // Should still be compressed. - assert x.header.get(.content_encoding)! == 'zstd' + assert x.header.get(.content_encoding)? == 'zstd' // Verify that .zst file was NOT created beside the source file. zst_path := '${readonly_file}.zst' @@ -380,8 +380,8 @@ fn test_precompressed_gz_file_served() { assert x.status() == .ok // Should serve the manually pre-compressed .gz file - assert x.header.get(.content_encoding)! == 'gzip' - assert x.header.get(.vary)! == 'Accept-Encoding' + assert x.header.get(.content_encoding)? == 'gzip' + assert x.header.get(.vary)? == 'Accept-Encoding' // HTTP client auto-decompresses gzip, so verify the content directly large_content := 'X'.repeat(2000) @@ -399,8 +399,8 @@ fn test_no_auto_compression_with_max_size_zero() { assert x.status() == .ok // Should serve the manually pre-compressed .gz file - assert x.header.get(.content_encoding)! == 'gzip' - assert x.header.get(.vary)! == 'Accept-Encoding' + assert x.header.get(.content_encoding)? == 'gzip' + assert x.header.get(.vary)? == 'Accept-Encoding' // HTTP client auto-decompresses gzip, so verify the content directly large_content := 'X'.repeat(2000) @@ -436,8 +436,8 @@ fn test_zstd_preferred_over_gzip() { x := req.do()! assert x.status() == .ok - assert x.header.get(.content_encoding)! == 'zstd', 'zstd should be preferred over gzip' - assert x.header.get(.vary)! == 'Accept-Encoding' + assert x.header.get(.content_encoding)? == 'zstd', 'zstd should be preferred over gzip' + assert x.header.get(.vary)? == 'Accept-Encoding' // Verify the body is valid zstd decompressed := zstd.decompress(x.body.bytes()) or { @@ -462,11 +462,11 @@ fn test_zst_file_cache_creation() { // Second request should use cached .zst file y := req.do()! assert y.status() == .ok - assert y.header.get(.content_encoding)! == 'zstd' + assert y.header.get(.content_encoding)? == 'zstd' // Verify Content-Length matches .zst file size zst_file_size := os.file_size(zst_path) - content_length := y.header.get(.content_length)!.u64() + content_length := y.header.get(.content_length)?.u64() assert content_length == zst_file_size, 'Content-Length should match .zst file size' } @@ -477,8 +477,8 @@ fn test_precompressed_zst_file_served() { x := req.do()! assert x.status() == .ok - assert x.header.get(.content_encoding)! == 'zstd' - assert x.header.get(.vary)! == 'Accept-Encoding' + assert x.header.get(.content_encoding)? == 'zstd' + assert x.header.get(.vary)? == 'Accept-Encoding' // Verify it's the pre-compressed content large_content := 'X'.repeat(2000) @@ -496,7 +496,7 @@ fn test_gzip_fallback_when_zstd_not_supported() { x := req.do()! assert x.status() == .ok - assert x.header.get(.content_encoding)! == 'gzip', 'should fallback to gzip when zstd not supported' + assert x.header.get(.content_encoding)? == 'gzip', 'should fallback to gzip when zstd not supported' // HTTP client auto-decompresses gzip, so verify the content directly assert x.body == test_file_content @@ -511,8 +511,8 @@ fn test_gzip_only_serves_gzip() { x := req.do()! assert x.status() == .ok - assert x.header.get(.content_encoding)! == 'gzip', 'gzip-only mode should serve gzip' - assert x.header.get(.vary)! == 'Accept-Encoding' + assert x.header.get(.content_encoding)? == 'gzip', 'gzip-only mode should serve gzip' + assert x.header.get(.vary)? == 'Accept-Encoding' // HTTP client auto-decompresses gzip, so verify the content directly assert x.body == test_file_content @@ -526,7 +526,7 @@ fn test_gzip_only_ignores_zstd_request() { assert x.status() == .ok // Should serve gzip, NOT zstd (because only enable_static_gzip is set) - assert x.header.get(.content_encoding)! == 'gzip', 'gzip-only mode should serve gzip even when client supports zstd' + assert x.header.get(.content_encoding)? == 'gzip', 'gzip-only mode should serve gzip even when client supports zstd' // HTTP client auto-decompresses gzip, so verify the content directly assert x.body == test_file_content @@ -557,8 +557,8 @@ fn test_zstd_only_serves_zstd() { x := req.do()! assert x.status() == .ok - assert x.header.get(.content_encoding)! == 'zstd', 'zstd-only mode should serve zstd' - assert x.header.get(.vary)! == 'Accept-Encoding' + assert x.header.get(.content_encoding)? == 'zstd', 'zstd-only mode should serve zstd' + assert x.header.get(.vary)? == 'Accept-Encoding' // Verify the body is valid zstd decompressed := zstd.decompress(x.body.bytes()) or { @@ -576,7 +576,7 @@ fn test_zstd_only_ignores_gzip_request() { assert x.status() == .ok // Should serve zstd, not gzip (because only enable_static_zstd is set) - assert x.header.get(.content_encoding)! == 'zstd', 'zstd-only mode should serve zstd even when client supports gzip' + assert x.header.get(.content_encoding)? == 'zstd', 'zstd-only mode should serve zstd even when client supports gzip' decompressed := zstd.decompress(x.body.bytes()) or { assert false, 'failed to decompress zstd response: ${err}' @@ -607,8 +607,8 @@ fn test_static_compression_mime_filter_allows_matching_types() { x := req.do()! assert x.status() == .ok - assert x.header.get(.content_encoding)! == 'gzip' - assert x.header.get(.vary)! == 'Accept-Encoding' + assert x.header.get(.content_encoding)? == 'gzip' + assert x.header.get(.vary)? == 'Accept-Encoding' assert x.body == filtered_css_content gz_path := find_cached_static_file('filtered.css', '.gz') diff --git a/vlib/veb/tests/static_handler_test.v b/vlib/veb/tests/static_handler_test.v index e8c31bf94..3dca17986 100644 --- a/vlib/veb/tests/static_handler_test.v +++ b/vlib/veb/tests/static_handler_test.v @@ -134,7 +134,7 @@ fn test_custom_mime_types() { x := http.get('${localserver}/unknown_mime.what')! assert x.status() == .ok - assert x.header.get(.content_type)! == veb.mime_types['.txt'] + assert x.header.get(.content_type)? == veb.mime_types['.txt'] assert x.body.trim_space() == 'unknown_mime' } @@ -164,7 +164,7 @@ fn test_markdown_negotiation_priority_first() { x := http.fetch(config)! assert x.status() == .ok - assert x.header.get(.content_type)! == 'text/markdown' + assert x.header.get(.content_type)? == 'text/markdown' assert x.body.contains('This is the about page in markdown format.') assert !x.body.contains('about.html.md variant') assert !x.body.contains('about/index.html.md variant') @@ -179,7 +179,7 @@ fn test_markdown_negotiation_priority_second() { x := http.fetch(config)! assert x.status() == .ok - assert x.header.get(.content_type)! == 'text/markdown' + assert x.header.get(.content_type)? == 'text/markdown' assert x.body.contains('# Page HTML Markdown') } @@ -192,7 +192,7 @@ fn test_markdown_negotiation_directory_index() { x := http.fetch(config)! assert x.status() == .ok - assert x.header.get(.content_type)! == 'text/markdown' + assert x.header.get(.content_type)? == 'text/markdown' assert x.body.contains('# Index HTML Markdown') } @@ -202,7 +202,7 @@ fn test_markdown_direct_access() { // Without Accept header x_no_header := http.get('${localserver}/test.md')! assert x_no_header.status() == .ok - assert x_no_header.header.get(.content_type)! == 'text/markdown' + assert x_no_header.header.get(.content_type)? == 'text/markdown' assert x_no_header.body.contains('# Test Markdown') // With Accept: text/markdown header - same result @@ -212,7 +212,7 @@ fn test_markdown_direct_access() { } x_with_header := http.fetch(config)! assert x_with_header.status() == .ok - assert x_with_header.header.get(.content_type)! == 'text/markdown' + assert x_with_header.header.get(.content_type)? == 'text/markdown' assert x_with_header.body.contains('# Test Markdown') } diff --git a/vlib/veb/tests/veb_test.v b/vlib/veb/tests/veb_test.v index 49f38c033..3600266c6 100644 --- a/vlib/veb/tests/veb_test.v +++ b/vlib/veb/tests/veb_test.v @@ -128,16 +128,16 @@ fn test_a_simple_tcp_client_html_page() { // net.http client based tests follow: fn assert_common_http_headers(x http.Response) ! { assert x.status() == .ok - assert x.header.get(.server)! == 'veb' - assert x.header.get(.content_length)!.int() > 0 + assert x.header.get(.server) or { '' } == 'veb' + assert (x.header.get(.content_length) or { '0' }).int() > 0 } fn test_http_client_index() { x := http.get('http://${localserver}/') or { panic(err) } assert_common_http_headers(x)! - assert x.header.get(.content_type)! == 'text/plain' + assert x.header.get(.content_type)? == 'text/plain' assert x.body == 'Welcome to veb' - assert x.header.get(.connection)! == 'close' + assert x.header.get(.connection)? == 'close' } fn test_http_client_404() { @@ -157,14 +157,14 @@ fn test_http_client_404() { fn test_http_client_simple() { x := http.get('http://${localserver}/simple') or { panic(err) } assert_common_http_headers(x)! - assert x.header.get(.content_type)! == 'text/plain' + assert x.header.get(.content_type)? == 'text/plain' assert x.body == 'A simple result' } fn test_http_client_html_page() { x := http.get('http://${localserver}/html_page') or { panic(err) } assert_common_http_headers(x)! - assert x.header.get(.content_type)! == 'text/html' + assert x.header.get(.content_type)? == 'text/html' assert x.body == '

ok

' } @@ -206,7 +206,7 @@ fn test_http_client_json_post() { $if debug_net_socket_client ? { eprintln('/json_echo endpoint response: ${x}') } - assert x.header.get(.content_type)! == 'application/json' + assert x.header.get(.content_type)? == 'application/json' assert x.body == json_for_ouser nuser := json.decode[User](x.body) or { User{} } assert '${ouser}' == '${nuser}' @@ -215,7 +215,7 @@ fn test_http_client_json_post() { $if debug_net_socket_client ? { eprintln('/json endpoint response: ${x}') } - assert x.header.get(.content_type)! == 'application/json' + assert x.header.get(.content_type)? == 'application/json' assert x.body == json_for_ouser nuser2 := json.decode[User](x.body) or { User{} } assert '${ouser}' == '${nuser2}' @@ -296,7 +296,7 @@ fn test_empty_response_body_has_content_length() { mut x := req.do()! assert x.status() == .ok - assert x.header.get(.content_length)! == '0' + assert x.header.get(.content_length)? == '0' } fn test_http_client_shutdown_does_not_work_without_a_cookie() { -- 2.39.5