From 5fc2edc671fc705652f32447a21705ec94f54bd7 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 18 Oct 2026 23:46:49 +0800 Subject: [PATCH] fix(fetch): classify 101 as final in response timing Reuse the existing interim status predicate in buffered, HTML and raw collectors so Switching Protocols starts the final response timeline. Cover 101 alone and after 103 across all collectors, plus HTML header completion. --- moli-fetch/src/blocking/collectors.rs | 10 ++-- .../src/runtime/response_timing_tests.rs | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/moli-fetch/src/blocking/collectors.rs b/moli-fetch/src/blocking/collectors.rs index acc40d2f6f..6db422d700 100644 --- a/moli-fetch/src/blocking/collectors.rs +++ b/moli-fetch/src/blocking/collectors.rs @@ -1078,7 +1078,7 @@ impl Handler for ResponseCollector { self.status_text = Some(parsed.status_text); self.body.clear(); if let Some(progress) = &self.response_progress { - if parsed.status < 200 { + if is_interim_response_status(parsed.status) { progress.mark_interim_response_started(); } else { progress.mark_response_started(); @@ -1155,7 +1155,7 @@ impl Handler for StreamingResponseCollector { self.declared_identity_body_length = None; self.response_too_large = false; self.cookie_set_reports.clear(); - if self.status < 200 { + if is_interim_response_status(self.status) { self.response_progress.mark_interim_response_started(); } else { self.response_progress.mark_response_started(); @@ -1233,10 +1233,10 @@ impl Handler for RawStreamingResponseCollector { self.response_bytes_received = 0; self.response_too_large = false; self.cookie_set_reports.clear(); - if self.status >= 200 { - self.response_progress.mark_response_started(); - } else { + if is_interim_response_status(self.status) { self.response_progress.mark_interim_response_started(); + } else { + self.response_progress.mark_response_started(); } return true; } diff --git a/moli-fetch/src/runtime/response_timing_tests.rs b/moli-fetch/src/runtime/response_timing_tests.rs index ac404bd75b..f122732ce4 100644 --- a/moli-fetch/src/runtime/response_timing_tests.rs +++ b/moli-fetch/src/runtime/response_timing_tests.rs @@ -177,6 +177,7 @@ async fn html_response_timing_records_terminal_failures() -> Result<()> { #[tokio::test] async fn html_response_timing_finishes_bodyless_responses_at_headers() -> Result<()> { for (method, headers) in [ + ("GET", "HTTP/1.1 101 Switching Protocols\r\n"), ("GET", "HTTP/1.1 204 No Content\r\n"), ("GET", "HTTP/1.1 205 Reset Content\r\n"), ("GET", "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n"), @@ -328,3 +329,56 @@ async fn html_declared_body_completion_precedes_delivery_of_the_last_chunk() -> } Ok(()) } + +#[test] +fn switching_protocols_timing_is_final_in_every_collector() { + for interim in [false, true] { + for mode in ["buffered", "html", "raw"] { + let cancel = FetchCancelHandle::new(); + let mut handler = match mode { + "buffered" => { + FetchTransferHandler::new_buffered(ResponseCollector::new(Some(cancel.clone()))) + } + "html" => { + let (start_tx, _start_rx) = oneshot::channel(); + let (body_tx, _body_rx) = mpsc::unbounded_channel(); + FetchTransferHandler::new_streaming(StreamingResponseCollector::new( + new_shared_browser_cookie_store(), + start_tx, + body_tx, + cancel.clone(), + )) + } + "raw" => { + let (start_tx, _start_rx) = oneshot::channel(); + let (body_tx, _body_rx) = mpsc::unbounded_channel(); + FetchTransferHandler::new_raw_streaming(RawStreamingResponseCollector::new( + new_shared_browser_cookie_store(), + start_tx, + body_tx, + cancel.clone(), + )) + } + _ => unreachable!(), + }; + if interim { + assert!(handler.header(b"HTTP/1.1 103 Early Hints\r\n")); + assert!(cancel.response_timing().response_started_at().is_none()); + } + assert!(handler.header(b"HTTP/1.1 101 Switching Protocols\r\n")); + let timing = cancel.response_timing(); + assert!( + timing.response_started_at().is_some(), + "{mode}: 101 must be final" + ); + assert_eq!( + timing.first_interim_response_started_at().is_some(), + interim, + "{mode}" + ); + if let Some(first_interim) = timing.first_interim_response_started_at() { + assert!(first_interim <= timing.response_started_at().unwrap()); + } + } + } +}