From 669c7bad4f4f6562a0122cb335a7c725e8a35d12 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 12 Sep 2026 04:30:55 +0800 Subject: [PATCH] fix(fetch): send zero content length for bodyless PUT requests --- moli-fetch/src/tests/mod.rs | 60 ++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/moli-fetch/src/tests/mod.rs b/moli-fetch/src/tests/mod.rs index da931dc674..548f3b5f7e 100644 --- a/moli-fetch/src/tests/mod.rs +++ b/moli-fetch/src/tests/mod.rs @@ -1739,7 +1739,7 @@ async fn request_headers_distinguish_empty_values_from_absent_upload_content_typ .parse::()?; seen.insert(index); let case = &cases[index]; - let (_, method, _, content_type) = case; + let (_, method, body, content_type) = case; assert!(request.starts_with(&format!("{method} /empty-headers/{index} HTTP/1.1\r\n"))); let values = |name: &str| { request @@ -1758,11 +1758,69 @@ async fn request_headers_distinguish_empty_values_from_absent_upload_content_typ content_type.iter().copied().collect::>(), "{case:?}: {request}" ); + if body.as_ref().is_none_or(Vec::is_empty) { + let expected_length = if body.is_some() || matches!(*method, "POST" | "PUT") { + vec!["0"] + } else { + vec![] + }; + assert_eq!( + values("content-length"), + expected_length, + "{case:?}: {request}" + ); + assert!( + values("transfer-encoding").is_empty(), + "{case:?}: {request}" + ); + } } assert_eq!(seen, (0..cases.len()).collect()); Ok(()) } +#[tokio::test] +async fn fetch_redirects_recompute_bodyless_put_content_length() -> Result<()> { + for status in [301, 302, 303, 307, 308] { + let server = ScriptedHttpServer::spawn(vec![ + ScriptedResponse::status(status, "Redirect").with_header("Location", "/final"), + ScriptedResponse::ok("done"), + ]); + let client = FetchClient::new(&FetchConfig::default(), new_shared_browser_cookie_store()); + let response = client + .fetch_raw(Request::new( + "PUT", + &server.url_path("/start"), + None, + vec![], + )?) + .await?; + assert_eq!(response.status, 200); + assert!(response.redirected); + let requests = server.requests(); + server.shutdown(); + assert_eq!(requests.len(), 2, "{status}: {requests:?}"); + let final_method = if status == 303 { "GET" } else { "PUT" }; + for (request, method, path) in [ + (&requests[0], "PUT", "/start"), + (&requests[1], final_method, "/final"), + ] { + assert!(request.starts_with(&format!("{method} {path} HTTP/1.1\r\n"))); + assert_eq!( + request_head_header_value(request, "content-length"), + if method == "PUT" { Some("0") } else { None }, + "{status}: {request}" + ); + assert_eq!(request_head_header_value(request, "content-type"), None); + assert_eq!( + request_head_header_value(request, "transfer-encoding"), + None + ); + } + } + Ok(()) +} + #[tokio::test] async fn fetch_redirect_303_rewrites_post_to_get_and_drops_body_headers() -> Result<()> { let server = ScriptedHttpServer::spawn(vec![