From cfc97efe514de437e2d45cbbb8bffeeac5bc26a7 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 9 Oct 2026 21:18:51 +0800 Subject: [PATCH] fix(fetch): preserve received HTTP status text Carry received HTTP reason phrases from the parser through buffered and streaming responses, disk and memory caches, page responses, Fetch, and XHR. Preserve custom and empty HTTP/1 phrases and report empty text for HTTP/2 and HTTP/3. Synthesized responses retain canonical fallback. Regressions cover whitespace, non-ASCII bytes, and cache round trips. --- moli-core/src/runtime/tests.rs | 1 + moli-fetch/src/blocking/cache/lookup.rs | 3 + moli-fetch/src/blocking/cache/metadata.rs | 7 +- moli-fetch/src/blocking/cache/revalidation.rs | 1 + moli-fetch/src/blocking/collectors.rs | 66 +++++-- moli-fetch/src/blocking/mod.rs | 2 + moli-fetch/src/proxy_connect.rs | 20 +-- moli-fetch/src/response.rs | 77 ++++++++ moli-fetch/src/runtime.rs | 21 ++- moli-fetch/src/streaming_response.rs | 9 + moli-fetch/src/tests/mod.rs | 169 ++++++++++++++++-- moli-http-cache/src/metadata.rs | 2 + moli-http-cache/src/tests.rs | 48 ++--- moli-page-types/src/lib.rs | 43 +++++ moli-protocol/src/conn/fetch_support.rs | 1 + .../src/conn/page_state/fetch_state.rs | 1 + moli-protocol/src/conn/runtime_load.rs | 4 + moli-protocol/src/domains/fetch/navigation.rs | 1 + .../src/domains/network/tests/cache.rs | 1 + .../src/document_script_scheduler.rs | 1 + .../context_host/child_documents/loads.rs | 1 + .../context_host/resource_loading.rs | 2 + .../src/network/backend/memory_cache.rs | 3 + .../src/network/devtools_resource_load.rs | 3 + .../src/network/request_client/tests.rs | 2 + moli-renderer-v8/src/network_host.rs | 2 - .../src/network_host/async_fetch.rs | 1 + .../src/network_host/browser_response.rs | 9 +- .../src/network_host/response/cors.rs | 2 + .../src/network_host/response/materialize.rs | 6 +- moli-renderer-v8/src/network_host/xhr.rs | 1 + .../src/network_host/xhr/delivery/pending.rs | 5 + .../src/network_host/xhr/delivery/response.rs | 2 +- .../src/network_host/xhr/instance_state.rs | 2 + .../src/runtime/browser_context_runtime.rs | 1 + .../runtime/page_vm/followed_navigation.rs | 1 + .../tests/async_subresource_completion.rs | 2 + .../tests/extracted/main_module_graphs.rs | 1 + .../subresource_fetch/pending_fetch.rs | 4 + .../browser_api/navigation/performance.rs | 1 + .../tests/dom_xhr/response_headers.rs | 1 + .../src/script_vm/tests/dom_xhr/xhr.rs | 11 ++ .../tests/extracted/child_documents.rs | 1 + .../script_vm/tests/extracted/page_runtime.rs | 1 + .../tests/extracted/realms_and_teardown.rs | 2 + moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../src/script_vm/tests/post_parse.rs | 1 + .../url_and_initializer_semantics.rs | 5 + .../src/service_worker_runtime/events.rs | 1 + .../service_worker_runtime/script_loading.rs | 2 + .../service/event_dispatch.rs | 1 + .../service/fetch_settlement.rs | 7 + .../src/worker/global_scope/fetch.rs | 7 + .../src/worker/thread/dispatch.rs | 2 + .../lifecycle/worker_and_fetch_lifecycle.rs | 4 + 55 files changed, 497 insertions(+), 79 deletions(-) diff --git a/moli-core/src/runtime/tests.rs b/moli-core/src/runtime/tests.rs index f1ab50af98..fcdb9baaec 100644 --- a/moli-core/src/runtime/tests.rs +++ b/moli-core/src/runtime/tests.rs @@ -81,6 +81,7 @@ async fn external_raw_bridge_drop_after_body_eof_cancels_pending_fetch_completio let (mut fetch_completion_tx, fetch_completion_rx) = oneshot::channel(); let response = StreamingRawResponse::new_with_head( ResponseHead { + status_text: None, final_url: Url::parse("https://bridge.test/document")?, status: 200, headers: vec![("content-type".to_owned(), b"text/html".to_vec())], diff --git a/moli-fetch/src/blocking/cache/lookup.rs b/moli-fetch/src/blocking/cache/lookup.rs index 7bba02dc0c..4163cb77d4 100644 --- a/moli-fetch/src/blocking/cache/lookup.rs +++ b/moli-fetch/src/blocking/cache/lookup.rs @@ -107,6 +107,7 @@ pub(crate) fn create_streaming_cache_body_writer_for_response_parts( request_url, request_url, status, + None, headers, false, ) else { @@ -122,6 +123,7 @@ pub(crate) fn finish_streaming_cached_response( cookie_header: Option<&str>, final_url: &Url, status: u16, + status_text: Option<&str>, headers: &[(String, Vec)], redirected: bool, writer: HttpCacheBodyWriter, @@ -136,6 +138,7 @@ pub(crate) fn finish_streaming_cached_response( request_url, final_url, status, + status_text, headers, redirected, ) else { diff --git a/moli-fetch/src/blocking/cache/metadata.rs b/moli-fetch/src/blocking/cache/metadata.rs index 54449825a4..61de03833d 100644 --- a/moli-fetch/src/blocking/cache/metadata.rs +++ b/moli-fetch/src/blocking/cache/metadata.rs @@ -13,6 +13,7 @@ pub(super) fn cache_metadata_for_response_parts( request_url: &Url, final_url: &Url, status: u16, + status_text: Option<&str>, headers: &[(String, Vec)], redirected: bool, ) -> Option { @@ -26,7 +27,7 @@ pub(super) fn cache_metadata_for_response_parts( redirected, )?; let vary_headers = vary_headers_for_response(config, request, request_url, headers)?; - Some(HttpCacheEntryMetadata::new( + let mut metadata = HttpCacheEntryMetadata::new( cache_request_url.to_string(), cache_final_url.to_string(), status, @@ -34,7 +35,9 @@ pub(super) fn cache_metadata_for_response_parts( unix_now_ms(), policy.expires_at_unix_ms, vary_headers, - )) + ); + metadata.status_text = status_text.map(str::to_owned); + Some(metadata) } pub(crate) fn response_headers_forbid_cache_storage(headers: &[(String, Vec)]) -> bool { diff --git a/moli-fetch/src/blocking/cache/revalidation.rs b/moli-fetch/src/blocking/cache/revalidation.rs index 22961cb67e..3d18abb40e 100644 --- a/moli-fetch/src/blocking/cache/revalidation.rs +++ b/moli-fetch/src/blocking/cache/revalidation.rs @@ -39,6 +39,7 @@ pub(crate) fn merge_cached_not_modified_streaming_response_lookup( request_url, &final_url, cached.status, + cached.metadata.status_text.as_deref(), &headers, false, ) { diff --git a/moli-fetch/src/blocking/collectors.rs b/moli-fetch/src/blocking/collectors.rs index 0b8c5bbc13..a8c7385d6b 100644 --- a/moli-fetch/src/blocking/collectors.rs +++ b/moli-fetch/src/blocking/collectors.rs @@ -15,6 +15,7 @@ use crate::{ FetchCancelHandle, FetchConfig, NegotiatedHttpVersion, NetworkRequestExtraInfo, Request, client_hints::{ClientHintResponseAction, ClientHintResponsePolicy}, headers::{decode_http_header_bytes, parse_http_response_header_line}, + response::parse_http_response_status_line, }; use crate::RedirectInfo; @@ -126,6 +127,7 @@ pub(crate) fn log_request_completion( pub(crate) struct ResponseCollector { body: Vec, headers: Vec<(String, Vec)>, + status_text: Option, max_response_size: Option, response_too_large: bool, cancel_handle: Option, @@ -142,10 +144,15 @@ impl ResponseCollector { pub(crate) fn begin_request(&mut self, max_response_size: Option) { self.body.clear(); self.headers.clear(); + self.status_text = None; self.max_response_size = max_response_size; self.response_too_large = false; } + pub(crate) fn status_text(&self) -> Option<&str> { + self.status_text.as_deref() + } + pub(crate) fn headers(&self) -> &[(String, Vec)] { &self.headers } @@ -159,6 +166,7 @@ impl ResponseCollector { pub struct StreamingResponseCollector { cookie_store: SharedBrowserCookieStore, headers: Vec<(String, Vec)>, + status_text: Option, current_url: Option, current_cookie_context: Option, status: u16, @@ -228,6 +236,7 @@ impl StreamingCachePlan { pub struct RawStreamingResponseCollector { cookie_store: SharedBrowserCookieStore, headers: Vec<(String, Vec)>, + status_text: Option, current_url: Option, current_cookie_context: Option, status: u16, @@ -268,6 +277,7 @@ impl StreamingResponseCollector { current_url: None, current_cookie_context: None, status: 0, + status_text: None, max_response_size: None, response_too_large: false, response_bytes_received: 0, @@ -303,6 +313,7 @@ impl StreamingResponseCollector { cache_body_writer: Option, ) { self.headers.clear(); + self.status_text = None; self.current_url = Some(current_url); self.current_cookie_context = Some(current_cookie_context); self.status = 0; @@ -350,6 +361,10 @@ impl StreamingResponseCollector { self.cache_plan = cache_plan; } + pub(crate) fn status_text(&self) -> Option<&str> { + self.status_text.as_deref() + } + pub fn headers(&self) -> &[(String, Vec)] { &self.headers } @@ -514,6 +529,7 @@ impl StreamingResponseCollector { let _ = start_tx.send(Ok(StreamingHtmlResponseStart { final_url: current_url, status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), request_cookie_report: self.request_cookie_report.clone(), cookie_set_reports: self.cookie_set_reports.clone(), @@ -644,6 +660,7 @@ impl RawStreamingResponseCollector { current_url: None, current_cookie_context: None, status: 0, + status_text: None, max_response_size: None, response_too_large: false, response_bytes_received: 0, @@ -679,6 +696,7 @@ impl RawStreamingResponseCollector { redirect_chain: Vec, ) { self.headers.clear(); + self.status_text = None; self.current_url = Some(current_url); self.current_cookie_context = Some(current_cookie_context); self.status = 0; @@ -729,6 +747,10 @@ impl RawStreamingResponseCollector { self.defer_not_modified_start = defer_not_modified_start; } + pub(crate) fn status_text(&self) -> Option<&str> { + self.status_text.as_deref() + } + pub fn headers(&self) -> &[(String, Vec)] { &self.headers } @@ -897,6 +919,7 @@ impl RawStreamingResponseCollector { let _ = start_tx.send(Ok(StreamingHtmlResponseStart { final_url: current_url, status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), request_cookie_report: self.request_cookie_report.clone(), cookie_set_reports: self.cookie_set_reports.clone(), @@ -1000,8 +1023,9 @@ impl Handler for ResponseCollector { return !self.response_too_large; } - if line.starts_with("HTTP/") { + if let Some(parsed) = parse_http_response_status_line(data) { self.headers.clear(); + self.status_text = Some(parsed.status_text); self.body.clear(); return true; } @@ -1060,15 +1084,12 @@ impl Handler for StreamingResponseCollector { return self.finalize_headers(); } - if let Some(status) = line - .strip_prefix("HTTP/") - .and_then(|rest| rest.split_whitespace().nth(1)) - .and_then(|status| status.parse::().ok()) - { + if let Some(parsed) = parse_http_response_status_line(data) { self.headers.clear(); self.headers_finalized = false; - self.status = status; - self.negotiated_http_version = NegotiatedHttpVersion::from_status_line(line); + self.status = parsed.status; + self.status_text = Some(parsed.status_text); + self.negotiated_http_version = parsed.version; self.response_bytes_received = 0; self.response_too_large = false; self.cookie_set_reports.clear(); @@ -1136,15 +1157,12 @@ impl Handler for RawStreamingResponseCollector { return self.finalize_headers(); } - if let Some(status) = line - .strip_prefix("HTTP/") - .and_then(|rest| rest.split_whitespace().nth(1)) - .and_then(|status| status.parse::().ok()) - { + if let Some(parsed) = parse_http_response_status_line(data) { self.headers.clear(); self.headers_finalized = false; - self.status = status; - self.negotiated_http_version = NegotiatedHttpVersion::from_status_line(line); + self.status = parsed.status; + self.status_text = Some(parsed.status_text); + self.negotiated_http_version = parsed.version; self.response_bytes_received = 0; self.response_too_large = false; self.cookie_set_reports.clear(); @@ -1206,6 +1224,24 @@ mod tests { use super::*; use moli_cookie_jar::new_shared_browser_cookie_store; + #[test] + fn buffered_status_message_tracks_final_response_and_resets_between_requests() { + let mut collector = ResponseCollector::new(None); + collector.begin_request(None); + assert!(collector.header(b"HTTP/1.1 103 Early Hints\r\n")); + assert!(collector.header(b"Link: ; rel=preload\r\n")); + assert!(collector.header(b"\r\n")); + assert!(collector.header(b"HTTP/1.1 200 \xa0caf\xe9\xff\r\n")); + assert!(collector.header(b"\r\n")); + assert_eq!(collector.status_text(), Some("\u{a0}caf\u{e9}\u{ff}")); + assert!(collector.headers().is_empty()); + + collector.begin_request(None); + assert_eq!(collector.status_text(), None); + assert!(collector.header(b"HTTP/1.1 200 \r\n")); + assert_eq!(collector.status_text(), Some("")); + } + fn raw_streaming_collector_with_headers( cancel_handle: FetchCancelHandle, headers: &[&str], diff --git a/moli-fetch/src/blocking/mod.rs b/moli-fetch/src/blocking/mod.rs index 1581d32eee..309cc4a982 100644 --- a/moli-fetch/src/blocking/mod.rs +++ b/moli-fetch/src/blocking/mod.rs @@ -66,6 +66,7 @@ pub(crate) enum RequestHttpVersion { pub struct StreamingHtmlResponseStart { pub final_url: Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, Vec)>, pub request_cookie_report: Option, pub cookie_set_reports: Vec, @@ -81,6 +82,7 @@ impl StreamingHtmlResponseStart { ResponseHead { final_url: self.final_url, status: self.status, + status_text: self.status_text, headers: self.headers, request_cookie_report: self.request_cookie_report, cookie_set_reports: self.cookie_set_reports, diff --git a/moli-fetch/src/proxy_connect.rs b/moli-fetch/src/proxy_connect.rs index 96c89aa514..f9df362a74 100644 --- a/moli-fetch/src/proxy_connect.rs +++ b/moli-fetch/src/proxy_connect.rs @@ -5,6 +5,7 @@ const MAX_PROXY_CONNECT_HEADER_BYTES: usize = 256 * 1024; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct ProxyConnectResponse { pub(crate) status: u16, + pub(crate) status_text: String, pub(crate) headers: Vec<(String, Vec)>, } @@ -13,6 +14,7 @@ pub(crate) struct ProxyConnectResponseRecorder { enabled: bool, collecting: bool, pending_status: Option, + pending_status_text: String, pending_headers: Vec<(String, Vec)>, observed_bytes: usize, completed: Option, @@ -23,6 +25,7 @@ impl ProxyConnectResponseRecorder { self.enabled = enabled; self.collecting = false; self.pending_status = None; + self.pending_status_text.clear(); self.pending_headers.clear(); self.observed_bytes = 0; self.completed = None; @@ -36,6 +39,7 @@ impl ProxyConnectResponseRecorder { } self.collecting = self.enabled; self.pending_status = None; + self.pending_status_text.clear(); self.pending_headers.clear(); self.observed_bytes = 0; self.completed = None; @@ -50,14 +54,16 @@ impl ProxyConnectResponseRecorder { if self.observed_bytes > MAX_PROXY_CONNECT_HEADER_BYTES { self.collecting = false; self.pending_status = None; + self.pending_status_text.clear(); self.pending_headers.clear(); return; } let line = decode_http_header_bytes(data); let line = line.trim_end_matches(['\r', '\n']); - if let Some(status) = parse_status_line(line) { - self.pending_status = Some(status); + if let Some(parsed) = crate::response::parse_http_response_status_line(data) { + self.pending_status = Some(parsed.status); + self.pending_status_text = parsed.status_text; self.pending_headers.clear(); return; } @@ -68,6 +74,7 @@ impl ProxyConnectResponseRecorder { .take() .map(|status| ProxyConnectResponse { status, + status_text: std::mem::take(&mut self.pending_status_text), headers: std::mem::take(&mut self.pending_headers), }); return; @@ -96,14 +103,6 @@ fn is_connect_request_header_block(data: &[u8]) -> bool { .is_some_and(|method| method.eq_ignore_ascii_case(b"CONNECT")) } -fn parse_status_line(line: &str) -> Option { - line.strip_prefix("HTTP/")? - .split_whitespace() - .nth(1)? - .parse() - .ok() -} - #[cfg(test)] mod tests { use super::*; @@ -132,6 +131,7 @@ mod tests { recorder.take_failed_response(407), Some(ProxyConnectResponse { status: 407, + status_text: "Proxy Authentication Required".to_owned(), headers: vec![( "Proxy-Authenticate".to_owned(), b"Basic realm=\"\xff\"".to_vec(), diff --git a/moli-fetch/src/response.rs b/moli-fetch/src/response.rs index 49893d2f3d..17f5563c51 100644 --- a/moli-fetch/src/response.rs +++ b/moli-fetch/src/response.rs @@ -41,6 +41,10 @@ impl NegotiatedHttpVersion { pub struct ResponseHead { pub final_url: Url, pub status: u16, + /// The received HTTP status message. `Some("")` is an explicitly empty + /// message, including HTTP/2 and HTTP/3 responses. `None` permits synthetic + /// responses without a supplied message to use the canonical phrase. + pub status_text: Option, pub headers: Vec<(String, Vec)>, pub request_cookie_report: Option, pub cookie_set_reports: Vec, @@ -54,6 +58,66 @@ impl ResponseHead { pub fn url_list(&self) -> crate::FetchUrlList<'_> { crate::FetchUrlList::new(&self.final_url, &self.redirect_chain) } + pub fn status_text(&self) -> &str { + self.status_text.as_deref().unwrap_or_else(|| { + if matches!( + self.negotiated_http_version, + Some(NegotiatedHttpVersion::Http2 | NegotiatedHttpVersion::Http3) + ) { + "" + } else { + http::StatusCode::from_u16(self.status) + .ok() + .and_then(|status| status.canonical_reason()) + .unwrap_or("") + } + }) + } +} + +pub(crate) struct HttpResponseStatusLine { + pub status: u16, + pub status_text: String, + pub version: Option, +} + +pub(crate) fn parse_http_response_status_line(data: &[u8]) -> Option { + if !data.starts_with(b"HTTP/") { + return None; + } + let line = data.strip_suffix(b"\n").unwrap_or(data); + let line = line.strip_suffix(b"\r").unwrap_or(line); + let boundary = line.iter().position(u8::is_ascii_whitespace)?; + let version = std::str::from_utf8(&line[..boundary]) + .ok() + .and_then(NegotiatedHttpVersion::from_status_line); + let rest = line[boundary..].trim_ascii_start(); + let code_end = rest + .iter() + .position(|byte| !byte.is_ascii_digit()) + .unwrap_or(rest.len()); + let status = std::str::from_utf8(&rest[..code_end]).ok()?.parse().ok()?; + let status_text = if matches!( + version, + Some(NegotiatedHttpVersion::Http2 | NegotiatedHttpVersion::Http3) + ) { + String::new() + } else { + // Status messages are ByteStrings. Preserve obs-text as Latin-1 and + // internal whitespace; Chromium only strips surrounding SP bytes. + rest[code_end..] + .iter() + .copied() + .map(char::from) + .collect::() + .trim_matches(' ') + .to_owned() + }; + Some(HttpResponseStatusLine { + status, + status_text, + version, + }) } #[derive(Debug)] @@ -255,6 +319,7 @@ impl ResponseBody { pub struct Response { pub final_url: Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, Vec)>, body: ResponseBody, pub request_cookie_report: Option, @@ -271,6 +336,7 @@ impl Clone for Response { Self { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), body: self .body @@ -328,6 +394,7 @@ impl Response { ResponseHead { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), request_cookie_report: self.request_cookie_report.clone(), cookie_set_reports: self.cookie_set_reports.clone(), @@ -364,6 +431,7 @@ impl Response { Ok(Self { final_url: head.final_url, status: head.status, + status_text: head.status_text, headers: head.headers, body, request_cookie_report: head.request_cookie_report, @@ -381,6 +449,7 @@ impl Response { Ok(Self { final_url: head.final_url, status: head.status, + status_text: head.status_text, headers: head.headers, body, request_cookie_report: head.request_cookie_report, @@ -397,6 +466,7 @@ impl Response { let head = ResponseHead { final_url: self.final_url, status: self.status, + status_text: self.status_text, headers: self.headers, request_cookie_report: self.request_cookie_report, cookie_set_reports: self.cookie_set_reports, @@ -433,6 +503,7 @@ impl Response { let head = ResponseHead { final_url: self.final_url, status: self.status, + status_text: self.status_text, headers: self.headers, request_cookie_report: self.request_cookie_report, cookie_set_reports: self.cookie_set_reports, @@ -457,6 +528,7 @@ impl Response { pub struct RawResponse { pub final_url: Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, Vec)>, body: ResponseBody, pub request_cookie_report: Option, @@ -473,6 +545,7 @@ impl Clone for RawResponse { Self { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), body: self .body @@ -524,6 +597,7 @@ impl RawResponse { ResponseHead { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), request_cookie_report: self.request_cookie_report.clone(), cookie_set_reports: self.cookie_set_reports.clone(), @@ -546,6 +620,7 @@ impl RawResponse { Ok(Self { final_url: head.final_url, status: head.status, + status_text: head.status_text, headers: head.headers, body: ResponseBody::materialized_bytes(body), request_cookie_report: head.request_cookie_report, @@ -563,6 +638,7 @@ impl RawResponse { Ok(Self { final_url: head.final_url, status: head.status, + status_text: head.status_text, headers: head.headers, body: ResponseBody::materialized_bytes(body), request_cookie_report: head.request_cookie_report, @@ -579,6 +655,7 @@ impl RawResponse { let head = ResponseHead { final_url: self.final_url, status: self.status, + status_text: self.status_text, headers: self.headers, request_cookie_report: self.request_cookie_report, cookie_set_reports: self.cookie_set_reports, diff --git a/moli-fetch/src/runtime.rs b/moli-fetch/src/runtime.rs index 9492cc7ca8..36641c8c99 100644 --- a/moli-fetch/src/runtime.rs +++ b/moli-fetch/src/runtime.rs @@ -1815,13 +1815,14 @@ impl RuntimeOwner { let final_url = job.current_url.clone(); let negotiated_http_version = negotiated_http_version_from_easy(&easy); - let (status, headers, cookie_set_reports, collector_http_version) = { + let (status, status_text, headers, cookie_set_reports, collector_http_version) = { let streaming = easy .get_mut() .streaming_mut() .expect("streaming request should use streaming collector"); ( streaming.status(), + streaming.status_text().map(str::to_owned), streaming.headers().to_vec(), streaming.take_cookie_set_reports(), streaming.negotiated_http_version(), @@ -1866,6 +1867,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -1928,6 +1930,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -2134,13 +2137,14 @@ impl RuntimeOwner { let final_url = job.current_url.clone(); let negotiated_http_version = negotiated_http_version_from_easy(&easy); - let (status, headers, cookie_set_reports, collector_http_version) = { + let (status, status_text, headers, cookie_set_reports, collector_http_version) = { let streaming = easy .get_mut() .raw_streaming_mut() .expect("raw streaming request should use raw streaming collector"); ( streaming.status(), + streaming.status_text().map(str::to_owned), streaming.headers().to_vec(), streaming.take_cookie_set_reports(), streaming.negotiated_http_version(), @@ -2256,6 +2260,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -2319,6 +2324,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -2328,6 +2334,7 @@ impl RuntimeOwner { } if let Some(started_tx) = started_tx { let _ = started_tx.send(Ok(StreamingHtmlResponseStart { + status_text: status_text.clone(), final_url, status, headers, @@ -2362,6 +2369,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -2886,6 +2894,7 @@ fn complete_streaming_html_job(job: StreamingRuntimeJob, response: Response) { let (head, body) = response.into_text_parts(); if let Some(started_tx) = job.started_tx { let _ = started_tx.send(Ok(StreamingHtmlResponseStart { + status_text: head.status_text, final_url: head.final_url, status: head.status, headers: head.headers, @@ -2914,6 +2923,7 @@ fn complete_cached_streaming_html_job( let redirected = !job.request.redirect_chain.is_empty(); let redirect_chain = job.request.redirect_chain.clone(); let CachedStreamingResponseLookup { + metadata, final_url, status, headers, @@ -2937,6 +2947,7 @@ fn complete_cached_streaming_html_job( if let Some(started_tx) = job.started_tx { let _ = started_tx.send(Ok(StreamingHtmlResponseStart { + status_text: metadata.status_text, final_url, status, headers, @@ -3034,6 +3045,7 @@ fn complete_cached_streaming_raw_job( let redirected = !job.request.redirect_chain.is_empty(); let redirect_chain = job.request.redirect_chain.clone(); let CachedStreamingResponseLookup { + metadata, final_url, status, headers, @@ -3057,6 +3069,7 @@ fn complete_cached_streaming_raw_job( if let Some(started_tx) = job.started_tx { let _ = started_tx.send(Ok(StreamingHtmlResponseStart { + status_text: metadata.status_text, final_url, status, headers, @@ -3500,6 +3513,7 @@ fn proxy_connect_response_start( StreamingHtmlResponseStart { final_url: current_url.clone(), status: response.status, + status_text: Some(response.status_text), headers: response.headers, request_cookie_report, cookie_set_reports: Vec::new(), @@ -3592,6 +3606,7 @@ fn collect_buffered_response( .get_ref() .buffered() .ok_or_else(|| anyhow!("curl runtime returned non-buffered easy for buffered request"))?; + let status_text = collector.status_text().map(str::to_owned); let headers = collector.headers().to_vec(); let body = collector.body().to_vec(); let transfer_metrics = transfer_metrics_from_easy(easy, &headers); @@ -3599,6 +3614,7 @@ fn collect_buffered_response( Ok(( RawResponse::from_head_and_body( ResponseHead { + status_text, final_url, status, headers, @@ -4258,6 +4274,7 @@ mod tests { job, Response::from_head_and_text_body( ResponseHead { + status_text: None, final_url: final_url.clone(), status: 200, headers: vec![("content-type".to_owned(), b"text/html".to_vec())], diff --git a/moli-fetch/src/streaming_response.rs b/moli-fetch/src/streaming_response.rs index ac68f6e677..a09d73d2eb 100644 --- a/moli-fetch/src/streaming_response.rs +++ b/moli-fetch/src/streaming_response.rs @@ -24,6 +24,7 @@ impl fmt::Debug for StreamingResponseLifetimeLease { pub struct StreamingHtmlResponse { pub final_url: Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, Vec)>, pub request_cookie_report: Option, pub cookie_set_reports: Vec, @@ -48,6 +49,7 @@ impl StreamingHtmlResponse { ) -> Self { Self::new_with_head( ResponseHead { + status_text: None, final_url, status, headers, @@ -73,6 +75,7 @@ impl StreamingHtmlResponse { Self { final_url: head.final_url, status: head.status, + status_text: head.status_text, headers: head.headers, request_cookie_report: head.request_cookie_report, cookie_set_reports: head.cookie_set_reports, @@ -91,6 +94,7 @@ impl StreamingHtmlResponse { ResponseHead { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), request_cookie_report: self.request_cookie_report.clone(), cookie_set_reports: self.cookie_set_reports.clone(), @@ -163,6 +167,7 @@ impl Drop for StreamingHtmlResponse { pub struct StreamingRawResponse { pub final_url: Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, Vec)>, pub request_cookie_report: Option, pub cookie_set_reports: Vec, @@ -193,6 +198,7 @@ impl StreamingRawResponse { Self { final_url, status, + status_text: None, headers, request_cookie_report, cookie_set_reports, @@ -217,6 +223,7 @@ impl StreamingRawResponse { Self { final_url: head.final_url, status: head.status, + status_text: head.status_text, headers: head.headers, request_cookie_report: head.request_cookie_report, cookie_set_reports: head.cookie_set_reports, @@ -307,6 +314,7 @@ impl StreamingRawResponse { ResponseHead { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), request_cookie_report: self.request_cookie_report.clone(), cookie_set_reports: self.cookie_set_reports.clone(), @@ -370,6 +378,7 @@ mod tests { fn sample_response_head() -> ResponseHead { ResponseHead { + status_text: None, final_url: Url::parse("http://example.test/final").expect("test URL"), status: 203, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], diff --git a/moli-fetch/src/tests/mod.rs b/moli-fetch/src/tests/mod.rs index ed45431f69..5224f4c89f 100644 --- a/moli-fetch/src/tests/mod.rs +++ b/moli-fetch/src/tests/mod.rs @@ -64,6 +64,7 @@ fn test_web_bot_auth_signer() -> WebBotAuthSigner { fn sample_response_head() -> ResponseHead { ResponseHead { + status_text: None, final_url: Url::parse("http://example.test/final").unwrap(), status: 203, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], @@ -115,22 +116,148 @@ fn fetch_raw_with_network_metadata_for_test( #[test] fn response_head_round_trips_through_text_and_raw_materialized_responses() { - let head = sample_response_head(); - let response = - Response::from_head_and_body(head.clone(), "hello".to_owned(), b"hello".to_vec()); + for status_text in [None, Some("Custom message"), Some("")] { + let mut head = sample_response_head(); + head.status_text = status_text.map(str::to_owned); + let response = + Response::from_head_and_body(head.clone(), "hello".to_owned(), b"hello".to_vec()); + assert_eq!(response.clone().head().status_text, head.status_text); + assert_eq!( + response.clone().into_parts().0.status_text, + head.status_text + ); - let raw = response.into_materialized_raw_response(); - assert_eq!(raw.final_url, head.final_url); - assert_eq!(raw.status, head.status); - assert_eq!(raw.headers, head.headers); - assert_eq!(raw.body_bytes(), b"hello"); + let raw = response.into_materialized_raw_response(); + assert_eq!(raw.final_url, head.final_url); + assert_eq!(raw.status, head.status); + assert_eq!(raw.clone().into_parts().0.status_text, head.status_text); + assert_eq!(raw.headers, head.headers); + assert_eq!(raw.body_bytes(), b"hello"); - let text = raw.into_lossy_materialized_text_response(); - assert_eq!(text.final_url, head.final_url); - assert_eq!(text.status, head.status); - assert_eq!(text.headers, head.headers); - assert_eq!(text.body_text(), "hello"); - assert_eq!(text.body_bytes(), b"hello"); + let text = raw.into_lossy_materialized_text_response(); + assert_eq!(text.final_url, head.final_url); + assert_eq!(text.status, head.status); + assert_eq!(text.head().status_text, head.status_text); + assert_eq!(text.headers, head.headers); + assert_eq!(text.body_text(), "hello"); + assert_eq!(text.body_bytes(), b"hello"); + } +} + +#[test] +fn http_status_messages_preserve_bytes_and_explicit_empty_values() { + for (line, status, expected) in [ + (b"HTTP/1.0 402 FIVE BUCKS\r\n".as_slice(), 402, "FIVE BUCKS"), + (b"HTTP/1.1 699 WAY OUTTA RANGE\r\n", 699, "WAY OUTTA RANGE"), + (b"HTTP/1.1 200\r\n", 200, ""), + ( + b"HTTP/1.1 200 Keep inner spaces \r\n", + 200, + "Keep inner spaces", + ), + (b"HTTP/1.1 200 \ttab\tinside\t \r\n", 200, "\ttab\tinside\t"), + ( + b"HTTP/1.1 200 \xa0caf\xe9\xff\r\n", + 200, + "\u{a0}caf\u{e9}\u{ff}", + ), + (b"HTTP/2 200 \r\n", 200, ""), + (b"HTTP/3 200 \r\n", 200, ""), + ] { + let parsed = crate::response::parse_http_response_status_line(line).unwrap(); + assert_eq!(parsed.status, status); + assert_eq!(parsed.status_text, expected); + } + + let mut head = sample_response_head(); + head.status = 200; + assert_eq!(head.status_text(), "OK"); + head.negotiated_http_version = Some(NegotiatedHttpVersion::Http2); + assert_eq!(head.status_text(), ""); + head.status_text = Some("Explicit override".to_owned()); + assert_eq!(head.status_text(), "Explicit override"); + head.negotiated_http_version = Some(NegotiatedHttpVersion::Http11); + head.status_text = Some(String::new()); + assert_eq!(head.status_text(), ""); +} + +#[tokio::test] +async fn http_status_messages_survive_buffered_and_streamed_transport() -> Result<()> { + for (status, received, expected) in [ + (200, "Different OK", "Different OK"), + (200, "", ""), + (200, " Keep inner spaces ", "Keep inner spaces"), + (200, "tab\tinside", "tab\tinside"), + (699, "WAY OUTTA RANGE", "WAY OUTTA RANGE"), + ] { + let server = ScriptedHttpServer::spawn(vec![ + ScriptedResponse::status(status, received) + .with_body("body"); + 3 + ]); + let client = FetchClient::new(&FetchConfig::default(), new_shared_browser_cookie_store()); + for mode in ["buffered", "html", "raw"] { + let request = Request::get(&server.url())?; + let (head, body) = match mode { + "buffered" => client + .fetch(request.with_follow_redirects(false)) + .await? + .into_body(), + "html" => client.fetch_html_stream(request).await?.into_body(), + _ => client + .fetch_raw_stream_with_cancel(request, FetchCancelHandle::new()) + .await? + .into_body(), + }; + assert_eq!(head.status, status, "{mode}"); + assert_eq!(head.status_text.as_deref(), Some(expected), "{mode}"); + assert_eq!(body.into_materialized_bytes().await?, b"body", "{mode}"); + } + assert_eq!(server.hits(), 3); + assert!(client.shutdown().is_clean()); + server.shutdown(); + } + Ok(()) +} + +#[tokio::test] +async fn http_status_messages_survive_disk_cache_across_streaming_modes() -> Result<()> { + for first_mode in ["html", "raw"] { + for status_text in ["Cached message", ""] { + let cache_dir = unique_test_cache_dir(); + let server = ScriptedHttpServer::spawn(vec![ + ScriptedResponse::status(200, status_text) + .with_header("Cache-Control", "max-age=60") + .with_body("cached body"), + ]); + let mut config = FetchConfig::default(); + config.set_http_cache_dir(Some(cache_dir.display().to_string())); + for (index, mode) in [first_mode, "html", "raw", "materialized"] + .into_iter() + .enumerate() + { + // Reopen the client so every subsequent read comes from disk. + let client = FetchClient::new(&config, new_shared_browser_cookie_store()); + let request = Request::get(&server.url())?; + let (head, body) = match mode { + "html" => client.fetch_html_stream(request).await?.into_body(), + "raw" => client + .fetch_raw_stream_with_cancel(request, FetchCancelHandle::new()) + .await? + .into_body(), + _ => client.fetch(request).await?.into_body(), + }; + assert_eq!(head.from_cache, index != 0, "{first_mode} to {mode}"); + assert_eq!(head.status_text.as_deref(), Some(status_text), "{mode}"); + assert_eq!(body.into_materialized_bytes().await?, b"cached body"); + assert!(client.shutdown().is_clean()); + } + assert_eq!(server.hits(), 1); + server.shutdown(); + fs::remove_dir_all(cache_dir)?; + } + } + Ok(()) } #[test] @@ -4664,7 +4791,8 @@ fn fetch_client_cache_revalidates_with_etag() { fn fetch_client_cache_updates_freshness_after_not_modified() { let cache_dir = unique_test_cache_dir(); let server = ScriptedHttpServer::spawn(vec![ - ScriptedResponse::ok("hit-1") + ScriptedResponse::status(200, "Original cached message") + .with_body("hit-1") .with_header("Cache-Control", "max-age=0") .with_header("ETag", "\"v1\""), ScriptedResponse::status(304, "Not Modified") @@ -4683,6 +4811,13 @@ fn fetch_client_cache_updates_freshness_after_not_modified() { assert_eq!(first.body_text(), "hit-1"); assert_eq!(second.body_text(), "hit-1"); assert_eq!(third.body_text(), "hit-1"); + for response in [&first, &second, &third] { + assert_eq!(response.status, 200); + assert_eq!( + response.status_text.as_deref(), + Some("Original cached message") + ); + } assert_eq!( server.hits(), 2, @@ -5586,6 +5721,8 @@ fn fetch_runtime_negotiates_http2_over_tls() { fetch_response_for_test(&client, Request::get(&server.url_path("/h2")).unwrap()).unwrap(); assert_eq!(response.body_text(), "h2-ok"); + assert_eq!(response.status_text.as_deref(), Some("")); + assert_eq!(response.head().status_text(), ""); assert_eq!( response.negotiated_http_version, Some(NegotiatedHttpVersion::Http2) @@ -5628,6 +5765,8 @@ async fn fetch_runtime_raw_stream_reports_negotiated_http2() -> Result<()> { FetchCancelHandle::new(), ) .await?; + assert_eq!(response.status_text.as_deref(), Some("")); + assert_eq!(response.head().status_text(), ""); assert_eq!( response.negotiated_http_version, Some(NegotiatedHttpVersion::Http2) diff --git a/moli-http-cache/src/metadata.rs b/moli-http-cache/src/metadata.rs index 998b6de695..be4ae60940 100644 --- a/moli-http-cache/src/metadata.rs +++ b/moli-http-cache/src/metadata.rs @@ -27,6 +27,7 @@ pub struct HttpCacheEntryMetadata { pub request_url: String, pub final_url: String, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, Vec)>, pub stored_at_unix_ms: u64, pub last_used_at_unix_ms: u64, @@ -51,6 +52,7 @@ impl HttpCacheEntryMetadata { request_url, final_url, status, + status_text: None, headers, stored_at_unix_ms, last_used_at_unix_ms: stored_at_unix_ms, diff --git a/moli-http-cache/src/tests.rs b/moli-http-cache/src/tests.rs index 461a4a0428..2c05fc2d87 100644 --- a/moli-http-cache/src/tests.rs +++ b/moli-http-cache/src/tests.rs @@ -679,6 +679,7 @@ fn missing_body_file_is_treated_as_incomplete_entry() -> Result<()> { request_url: "http://example.test/cache".to_owned(), final_url: "http://example.test/cache".to_owned(), status: 200, + status_text: None, headers: Vec::new(), stored_at_unix_ms: 1, last_used_at_unix_ms: 1, @@ -727,29 +728,31 @@ fn unsupported_metadata_version_is_ignored_and_removed() -> Result<()> { let store = HttpCacheStore::new(&root); let key = HttpCacheStore::key_for_url("http://example.test/cache"); let entry_dir = store.entry_dir(&key); - fs::create_dir_all(&entry_dir)?; - let meta_path = entry_dir.join(META_FILE); - fs::write( - &meta_path, - serde_json::to_vec(&serde_json::json!({ - "version": 99, - "request_url": "http://example.test/cache", - "final_url": "http://example.test/cache", - "status": 200, - "headers": [], - "stored_at_unix_ms": 1, - "last_used_at_unix_ms": 1, - "expires_at_unix_ms": 2, - "vary_headers": [], - "body_file": "body.unsupported.bin" - }))?, - )?; + for version in [3, 99] { + fs::create_dir_all(&entry_dir)?; + let meta_path = entry_dir.join(META_FILE); + fs::write( + &meta_path, + serde_json::to_vec(&serde_json::json!({ + "version": version, + "request_url": "http://example.test/cache", + "final_url": "http://example.test/cache", + "status": 200, + "headers": [], + "stored_at_unix_ms": 1, + "last_used_at_unix_ms": 1, + "expires_at_unix_ms": 2, + "vary_headers": [], + "body_file": "body.unsupported.bin" + }))?, + )?; - assert!(load_test_entry(&store, &key)?.is_none()); - assert!( - !meta_path.exists(), - "unsupported cache metadata version should be removed" - ); + assert!(load_test_entry(&store, &key)?.is_none()); + assert!( + !meta_path.exists(), + "unsupported cache metadata version should be removed" + ); + } let _ = fs::remove_dir_all(root); Ok(()) @@ -801,6 +804,7 @@ fn metadata_body_file_must_not_escape_entry_directory() -> Result<()> { request_url: "http://example.test/cache".to_owned(), final_url: "http://example.test/cache".to_owned(), status: 200, + status_text: None, headers: Vec::new(), stored_at_unix_ms: 1, last_used_at_unix_ms: 1, diff --git a/moli-page-types/src/lib.rs b/moli-page-types/src/lib.rs index 1d85a92ec1..3964c53043 100644 --- a/moli-page-types/src/lib.rs +++ b/moli-page-types/src/lib.rs @@ -318,6 +318,7 @@ impl From for RedirectInfo { pub struct NavigationResponse { pub final_url: Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, Vec)>, body: ResponseBody, pub request_cookie_report: Option, @@ -334,6 +335,7 @@ impl Clone for NavigationResponse { Self { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), body: self .body @@ -393,6 +395,7 @@ impl NavigationResponse { ResponseHead { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), request_cookie_report: self.request_cookie_report.clone(), cookie_set_reports: self.cookie_set_reports.clone(), @@ -437,6 +440,7 @@ impl NavigationResponse { Self { final_url: head.final_url, status: head.status, + status_text: head.status_text, headers: head.headers, body, request_cookie_report: head.request_cookie_report, @@ -461,6 +465,7 @@ impl NavigationResponse { ) -> Self { Self::from_head_and_text_body( ResponseHead { + status_text: None, final_url, status, headers, @@ -481,6 +486,9 @@ impl NavigationResponse { headers: Vec<(String, Vec)>, ) -> Self { let mut head = source.head(); + if head.status != status { + head.status_text = None; + } head.status = status; head.headers = headers; Self::from_head_and_materialized_body(head, source.materialized_body()) @@ -491,6 +499,7 @@ impl NavigationResponse { let head = ResponseHead { final_url: self.final_url, status: self.status, + status_text: self.status_text, headers: self.headers, request_cookie_report: self.request_cookie_report, cookie_set_reports: self.cookie_set_reports, @@ -527,6 +536,7 @@ impl NavigationResponse { let head = ResponseHead { final_url: self.final_url, status: self.status, + status_text: self.status_text, headers: self.headers, request_cookie_report: self.request_cookie_report, cookie_set_reports: self.cookie_set_reports, @@ -3514,6 +3524,36 @@ mod tests { ) } + #[test] + fn navigation_status_message_survives_conversion_and_header_overrides() { + for status_text in [None, Some("Original message"), Some("")] { + let mut response = NavigationResponse::from_text_body( + test_url("/response"), + 200, + Vec::new(), + "body".to_owned(), + ); + response.status_text = status_text.map(str::to_owned); + let headers = vec![("x-overridden".to_owned(), b"true".to_vec())]; + let overridden = + NavigationResponse::with_status_headers_from(&response, 200, headers.clone()); + assert_eq!(overridden.headers, headers); + assert_eq!(overridden.status_text, response.status_text); + assert_eq!( + overridden.clone().into_parts().0.status_text, + response.status_text + ); + let text = Response::from(overridden); + assert_eq!(text.head().status_text, response.status_text); + let round_trip = NavigationResponse::from(text); + assert_eq!(round_trip.into_body().0.status_text, response.status_text); + + let changed = NavigationResponse::with_status_headers_from(&response, 201, Vec::new()); + assert_eq!(changed.head().status_text(), "Created"); + assert_eq!(changed.body_text(), "body"); + } + } + #[test] fn navigation_response_byte_parts_transfer_exact_storage() { let template = @@ -3984,6 +4024,7 @@ mod tests { fn subresource_response_body_stores_only_exact_response_bytes() { let response = Response::from_head_and_body( ResponseHead { + status_text: None, final_url: test_url("/utf8.txt"), status: 200, headers: Vec::new(), @@ -4010,6 +4051,7 @@ mod tests { fn subresource_response_body_preserves_non_utf8_response_bytes() { let response = Response::from_head_and_body( ResponseHead { + status_text: None, final_url: test_url("/legacy.txt"), status: 200, headers: Vec::new(), @@ -4049,6 +4091,7 @@ mod tests { ResponseHead { final_url: test_url("/image.png"), status: 200, + status_text: None, headers: vec![("Content-Type".to_owned(), b"image/png".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), diff --git a/moli-protocol/src/conn/fetch_support.rs b/moli-protocol/src/conn/fetch_support.rs index ffc58e8370..7d915e2a9a 100644 --- a/moli-protocol/src/conn/fetch_support.rs +++ b/moli-protocol/src/conn/fetch_support.rs @@ -261,6 +261,7 @@ impl PendingFetchAuthNavigation { Arc::new(NetworkFetchResult::without_request_observation( RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: url, status: 401, headers: vec![( diff --git a/moli-protocol/src/conn/page_state/fetch_state.rs b/moli-protocol/src/conn/page_state/fetch_state.rs index c08a20776c..afe455a3df 100644 --- a/moli-protocol/src/conn/page_state/fetch_state.rs +++ b/moli-protocol/src/conn/page_state/fetch_state.rs @@ -398,6 +398,7 @@ mod tests { request_headers: Vec::new().into(), response: RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: url.clone(), status: 200, headers: vec![("content-type".to_owned(), b"text/html".to_vec())], diff --git a/moli-protocol/src/conn/runtime_load.rs b/moli-protocol/src/conn/runtime_load.rs index c80b9bc7fe..0545b00b98 100644 --- a/moli-protocol/src/conn/runtime_load.rs +++ b/moli-protocol/src/conn/runtime_load.rs @@ -147,6 +147,7 @@ async fn prepare_browser_owned_error_page_navigation_with_engine_async( .expect("the browser-owned network error page URL must be valid"); let error_page = NetworkErrorPageNavigation::new(error_text, unreachable_url.clone()); let head = ResponseHead { + status_text: None, final_url: error_page_url, status: 200, headers: vec![( @@ -1201,6 +1202,7 @@ fn decoded_data_url_navigation_response( let requested_url = Url::parse(raw_url).context("failed to parse data url")?; let response = RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: requested_url.clone(), status: 200, headers: vec![( @@ -1300,6 +1302,7 @@ async fn load_inline_html_navigation_with_engine_async( response_headers, } = source?; let head = ResponseHead { + status_text: None, final_url: document_url.clone(), status: 200, headers: response_headers, @@ -2988,6 +2991,7 @@ impl CdpConnection { let response_cookie_reports = load_inputs.store_response_cookie_reports(&final_url, &response_headers); let head = ResponseHead { + status_text: None, final_url, status: response_status, headers: response_headers, diff --git a/moli-protocol/src/domains/fetch/navigation.rs b/moli-protocol/src/domains/fetch/navigation.rs index d1ab7c312d..b2c1d8688b 100644 --- a/moli-protocol/src/domains/fetch/navigation.rs +++ b/moli-protocol/src/domains/fetch/navigation.rs @@ -616,6 +616,7 @@ fn pause_data_url_response_stage_navigation_into_buffer( }; let response = RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: pending.navigation.requested_url.clone(), status: 200, headers: vec![( diff --git a/moli-protocol/src/domains/network/tests/cache.rs b/moli-protocol/src/domains/network/tests/cache.rs index f6b458a3d0..3c3d2ade9a 100644 --- a/moli-protocol/src/domains/network/tests/cache.rs +++ b/moli-protocol/src/domains/network/tests/cache.rs @@ -679,6 +679,7 @@ async fn clear_browser_cache_keeps_pending_response_navigation_transfer() { request_headers: Vec::new().into(), response: RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: url, status: 200, headers: Vec::new(), diff --git a/moli-renderer-v8/src/document_script_scheduler.rs b/moli-renderer-v8/src/document_script_scheduler.rs index 797aaf5af7..2fd3ef2985 100644 --- a/moli-renderer-v8/src/document_script_scheduler.rs +++ b/moli-renderer-v8/src/document_script_scheduler.rs @@ -1094,6 +1094,7 @@ mod tests { ); let response = crate::types::NavigationResponse::from_head_and_text_body( moli_fetch::ResponseHead { + status_text: None, final_url: module_script.url.clone(), status: 200, headers: vec![("content-type".to_owned(), b"text/javascript".to_vec())], diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs index b3f06bb750..e3e4d2b043 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs @@ -873,6 +873,7 @@ mod tests { "GET".to_owned(), Vec::new(), moli_fetch::ResponseHead { + status_text: None, final_url: url::Url::parse("https://example.test/child").unwrap(), status: 200, headers: vec![("Content-Type".to_owned(), b"text/html".to_vec())], diff --git a/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs b/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs index 7a3f0975a0..917f9226ce 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs @@ -2885,6 +2885,7 @@ impl JsContextHost { response: navigation_response_from_subresource_body( &info.response_body, moli_fetch::ResponseHead { + status_text: None, final_url: info.url.clone(), status: info.response_status, headers: info.response_headers.clone(), @@ -2929,6 +2930,7 @@ impl JsContextHost { response: navigation_response_from_subresource_body( &info.response_body, moli_fetch::ResponseHead { + status_text: None, final_url: info.response_final_url.clone(), status: info.response_status, headers: info.response_headers.clone(), diff --git a/moli-renderer-v8/src/network/backend/memory_cache.rs b/moli-renderer-v8/src/network/backend/memory_cache.rs index 856b4123d7..0ded03479d 100644 --- a/moli-renderer-v8/src/network/backend/memory_cache.rs +++ b/moli-renderer-v8/src/network/backend/memory_cache.rs @@ -822,6 +822,7 @@ mod tests { fn response(url: &str, body: &str) -> Response { Response::from_head_and_text_body( ResponseHead { + status_text: None, final_url: Url::parse(url).expect("response URL"), status: 200, headers: vec![("cache-control".to_owned(), b"max-age=60".to_vec())], @@ -839,6 +840,7 @@ mod tests { fn raw_response(url: &str, body: &[u8]) -> RawResponse { RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: Url::parse(url).expect("response URL"), status: 200, headers: vec![("cache-control".to_owned(), b"max-age=60".to_vec())], @@ -1001,6 +1003,7 @@ mod tests { }; let response = Response::from_head_and_body( ResponseHead { + status_text: None, final_url: request.url.clone(), status: 200, headers: vec![("cache-control".to_owned(), b"max-age=60".to_vec())], diff --git a/moli-renderer-v8/src/network/devtools_resource_load.rs b/moli-renderer-v8/src/network/devtools_resource_load.rs index 58676fd664..3c1a630668 100644 --- a/moli-renderer-v8/src/network/devtools_resource_load.rs +++ b/moli-renderer-v8/src/network/devtools_resource_load.rs @@ -61,6 +61,7 @@ impl RendererPreparedNetworkResourceLoad { RendererNetworkResourceLoadResponse { final_url: head.final_url, status: head.status, + status_text: head.status_text, headers: head.headers, body, completion_error, @@ -83,6 +84,7 @@ pub enum RendererNetworkResourceLoadOutcome { pub struct RendererNetworkResourceLoadResponse { pub final_url: url::Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, Vec)>, pub body: Vec, pub completion_error: Option, @@ -99,6 +101,7 @@ impl RendererNetworkResourceLoadResponse { moli_fetch::ResponseHead { final_url: self.final_url.clone(), status: self.status, + status_text: self.status_text.clone(), headers: self.headers.clone(), request_cookie_report: self.request_cookie_report.clone(), cookie_set_reports: self.cookie_set_reports.clone(), diff --git a/moli-renderer-v8/src/network/request_client/tests.rs b/moli-renderer-v8/src/network/request_client/tests.rs index d83969f9d4..cb0e5d23ab 100644 --- a/moli-renderer-v8/src/network/request_client/tests.rs +++ b/moli-renderer-v8/src/network/request_client/tests.rs @@ -278,6 +278,7 @@ async fn memory_cache_tee_drop_after_body_eof_cancels_pending_completion_and_rel let (mut completion_tx, completion_rx) = oneshot::channel(); let inner = StreamingRawResponse::new_with_head( ResponseHead { + status_text: None, final_url: request.url.clone(), status: 200, headers: vec![("cache-control".to_owned(), b"max-age=60".to_vec())], @@ -1577,6 +1578,7 @@ async fn cached_raw_subresource_marks_redirect_hops_from_cache() -> Result<()> { let final_url = Url::parse("http://example.test/final.txt")?; let response = RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: final_url.clone(), status: 200, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], diff --git a/moli-renderer-v8/src/network_host.rs b/moli-renderer-v8/src/network_host.rs index 201402295d..dcf09b0893 100644 --- a/moli-renderer-v8/src/network_host.rs +++ b/moli-renderer-v8/src/network_host.rs @@ -21,7 +21,6 @@ mod text_track; mod url_helpers; mod xhr; -use http::StatusCode; use moli_fetch::{Request, Response, observe_cookie_access_report_for_request}; use moli_webapi_declare::WebApiObject; @@ -65,7 +64,6 @@ pub(crate) use self::body_source::{ error_pending_network_body_stream_with_reason, new_network_body_source_id, pending_network_body_stream, retain_pending_network_body_state_in_retired_realm, }; -pub(in crate::network_host) use self::browser_response::http_status_text; pub(crate) use self::browser_response::{ LocalUrlError, local_url_response, local_url_response_result, }; diff --git a/moli-renderer-v8/src/network_host/async_fetch.rs b/moli-renderer-v8/src/network_host/async_fetch.rs index aef9a5bff3..ad249e5493 100644 --- a/moli-renderer-v8/src/network_host/async_fetch.rs +++ b/moli-renderer-v8/src/network_host/async_fetch.rs @@ -922,6 +922,7 @@ mod tests { let transition = redirects .advance( ResponseHead { + status_text: None, final_url: Url::parse("https://origin.test/start")?, status: 303, headers: vec![("Location".to_owned(), b"https://target.test/final".to_vec())], diff --git a/moli-renderer-v8/src/network_host/browser_response.rs b/moli-renderer-v8/src/network_host/browser_response.rs index e2a9617adf..0e74ba77c5 100644 --- a/moli-renderer-v8/src/network_host/browser_response.rs +++ b/moli-renderer-v8/src/network_host/browser_response.rs @@ -21,13 +21,6 @@ impl fmt::Display for LocalUrlError { impl std::error::Error for LocalUrlError {} -pub(in crate::network_host) fn http_status_text(status: u16) -> &'static str { - StatusCode::from_u16(status) - .ok() - .and_then(|status| status.canonical_reason()) - .unwrap_or("") -} - pub(crate) fn blob_url_response(url: &url::Url) -> Option { let (body_bytes, mime_type) = blob::object_url_bytes_and_type(url.as_str())?; let headers = moli_fetch::headers_from_byte_strings(&[ @@ -37,6 +30,7 @@ pub(crate) fn blob_url_response(url: &url::Url) -> Option { .expect("Blob response headers contain ByteStrings"); Some(Response::from_head_and_lossy_body_bytes( moli_fetch::ResponseHead { + status_text: None, final_url: url.clone(), status: 200, headers, @@ -55,6 +49,7 @@ pub(crate) fn data_url_response(url: &url::Url) -> Option { let (body_bytes, mime_type) = data_url_body_and_mime_type(url.as_str())?; Some(Response::from_head_and_lossy_body_bytes( moli_fetch::ResponseHead { + status_text: None, final_url: url.clone(), status: 200, headers: vec![( diff --git a/moli-renderer-v8/src/network_host/response/cors.rs b/moli-renderer-v8/src/network_host/response/cors.rs index 05312024b1..35e446399b 100644 --- a/moli-renderer-v8/src/network_host/response/cors.rs +++ b/moli-renderer-v8/src/network_host/response/cors.rs @@ -566,6 +566,7 @@ mod tests { headers: Vec<(String, Vec)>, ) -> moli_fetch::ResponseHead { moli_fetch::ResponseHead { + status_text: None, final_url, status: 200, headers, @@ -582,6 +583,7 @@ mod tests { fn cors_response_chain_checks_network_redirects_without_extra_info() { for from_cache in [false, true] { let mut head = moli_fetch::ResponseHead { + status_text: None, final_url: url("https://final.test/script.js"), status: 200, headers: vec![("Access-Control-Allow-Origin".to_owned(), b"*".to_vec())], diff --git a/moli-renderer-v8/src/network_host/response/materialize.rs b/moli-renderer-v8/src/network_host/response/materialize.rs index e4193d2cc5..14c2f10efb 100644 --- a/moli-renderer-v8/src/network_host/response/materialize.rs +++ b/moli-renderer-v8/src/network_host/response/materialize.rs @@ -176,9 +176,9 @@ fn filtered_response_exposes_headers(filter: FetchResponseFilter) -> bool { fn filtered_response_status_text( head: &moli_fetch::ResponseHead, filter: FetchResponseFilter, -) -> &'static str { +) -> &str { if filter.is_readable() { - http_status_text(head.status) + head.status_text() } else { "" } @@ -392,7 +392,7 @@ fn build_fetch_response_object_head<'s>( scope, obj, RESPONSE_INTERNAL_STATUS_TEXT_SLOT, - http_status_text(head.status), + head.status_text(), ); let internal_headers = filter_headers_for_guard( &moli_fetch::headers_to_byte_strings(&head.headers), diff --git a/moli-renderer-v8/src/network_host/xhr.rs b/moli-renderer-v8/src/network_host/xhr.rs index 14d5b0772c..54eb60232b 100644 --- a/moli-renderer-v8/src/network_host/xhr.rs +++ b/moli-renderer-v8/src/network_host/xhr.rs @@ -38,6 +38,7 @@ pub(crate) const XHR_UPLOAD_IN_PROGRESS_SLOT: &str = "__lmXhrUploadInProgress"; const XHR_UPLOAD_LISTENER_SLOT: &str = "__lmXhrUploadListener"; const XHR_PENDING_KIND_SLOT: &str = "__lmXhrPendingKind"; const XHR_PENDING_STATUS_SLOT: &str = "__lmXhrPendingStatus"; +const XHR_PENDING_STATUS_TEXT_SLOT: &str = "__lmXhrPendingStatusText"; const XHR_PENDING_URL_SLOT: &str = "__lmXhrPendingUrl"; const XHR_PENDING_BODY_SLOT: &str = "__lmXhrPendingBody"; const XHR_PENDING_BODY_BYTES_SLOT: &str = "__lmXhrPendingBodyBytes"; diff --git a/moli-renderer-v8/src/network_host/xhr/delivery/pending.rs b/moli-renderer-v8/src/network_host/xhr/delivery/pending.rs index fdaa5ed040..6c282d21c1 100644 --- a/moli-renderer-v8/src/network_host/xhr/delivery/pending.rs +++ b/moli-renderer-v8/src/network_host/xhr/delivery/pending.rs @@ -15,6 +15,7 @@ pub(in crate::network_host::xhr) fn queue_xhr_response_delivery( .unwrap_or(XmlHttpRequestResponseType::Default); set_xhr_state_string(scope, xhr, XHR_PENDING_KIND_SLOT, "response"); set_xhr_state_number(scope, xhr, XHR_PENDING_STATUS_SLOT, head.status as f64); + set_xhr_state_string(scope, xhr, XHR_PENDING_STATUS_TEXT_SLOT, head.status_text()); set_xhr_state_string(scope, xhr, XHR_PENDING_URL_SLOT, head.final_url.as_str()); match response_type { XmlHttpRequestResponseType::ArrayBuffer | XmlHttpRequestResponseType::Blob => { @@ -96,6 +97,8 @@ fn xhr_complete_callback( let kind = xhr_state_string_property(scope, xhr, XHR_PENDING_KIND_SLOT).unwrap_or_default(); let pending_status = xhr_state_number_property(scope, xhr, XHR_PENDING_STATUS_SLOT).unwrap_or(0.0) as u16; + let pending_status_text = + xhr_state_string_property(scope, xhr, XHR_PENDING_STATUS_TEXT_SLOT).unwrap_or_default(); let pending_url = xhr_state_string_property(scope, xhr, XHR_PENDING_URL_SLOT).unwrap_or_default(); let pending_body_value = xhr_state_value(scope, xhr, XHR_PENDING_BODY_BYTES_SLOT) @@ -131,6 +134,7 @@ fn xhr_complete_callback( scope, xhr, moli_fetch::ResponseHead { + status_text: Some(pending_status_text), final_url, status: pending_status, headers, @@ -154,6 +158,7 @@ fn xhr_complete_callback( fn xhr_clear_pending(scope: &mut v8::PinScope<'_, '_>, xhr: v8::Local<'_, v8::Object>) { set_xhr_state_string(scope, xhr, XHR_PENDING_KIND_SLOT, ""); set_xhr_state_number(scope, xhr, XHR_PENDING_STATUS_SLOT, 0.0); + set_xhr_state_string(scope, xhr, XHR_PENDING_STATUS_TEXT_SLOT, ""); set_xhr_state_string(scope, xhr, XHR_PENDING_URL_SLOT, ""); set_xhr_state_string(scope, xhr, XHR_PENDING_BODY_SLOT, ""); set_xhr_state_number(scope, xhr, XHR_PENDING_BODY_LENGTH_SLOT, 0.0); diff --git a/moli-renderer-v8/src/network_host/xhr/delivery/response.rs b/moli-renderer-v8/src/network_host/xhr/delivery/response.rs index d5f95b3329..f978b0c5d9 100644 --- a/moli-renderer-v8/src/network_host/xhr/delivery/response.rs +++ b/moli-renderer-v8/src/network_host/xhr/delivery/response.rs @@ -419,7 +419,7 @@ fn set_xhr_response_head( scope, xhr, XHR_STATUS_TEXT_SLOT, - status_text.unwrap_or_else(|| http_status_text(head.status)), + status_text.unwrap_or_else(|| head.status_text()), ); let response_url = head .redirect_chain diff --git a/moli-renderer-v8/src/network_host/xhr/instance_state.rs b/moli-renderer-v8/src/network_host/xhr/instance_state.rs index 82f4775f3d..c298e48665 100644 --- a/moli-renderer-v8/src/network_host/xhr/instance_state.rs +++ b/moli-renderer-v8/src/network_host/xhr/instance_state.rs @@ -89,6 +89,8 @@ struct XmlHttpRequestStateDeclaration { active_internal_id: (), #[webapi(slot = XHR_PENDING_STATUS_SLOT, init = 0)] pending_status: (), + #[webapi(slot = XHR_PENDING_STATUS_TEXT_SLOT, init = "")] + pending_status_text: (), #[webapi(slot = XHR_TIMEOUT_SLOT, init = 0)] timeout: (), #[webapi(slot = XHR_TIMEOUT_START_MS_SLOT, init = 0)] diff --git a/moli-renderer-v8/src/runtime/browser_context_runtime.rs b/moli-renderer-v8/src/runtime/browser_context_runtime.rs index 9118db8b8e..e352f8ece9 100644 --- a/moli-renderer-v8/src/runtime/browser_context_runtime.rs +++ b/moli-renderer-v8/src/runtime/browser_context_runtime.rs @@ -303,6 +303,7 @@ impl DetachedParserScriptFetchContinuation { let text = String::from_utf8_lossy(&response_body).into_owned(); let response = crate::protocol_types::NavigationResponse::from_head_and_materialized_body( ResponseHead { + status_text: None, final_url: inner.script.url.clone(), status: response_code, headers: response_headers, diff --git a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs index 247f3e8bac..edd6a0696f 100644 --- a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs +++ b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs @@ -480,6 +480,7 @@ fn about_blank_navigation_response(url: &Url) -> Option { } Some(moli_fetch::Response::from_head_and_lossy_body_bytes( moli_fetch::ResponseHead { + status_text: None, final_url: url.clone(), status: 200, headers: vec![( diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/async_subresource_completion.rs b/moli-renderer-v8/src/runtime/page_vm/tests/async_subresource_completion.rs index 56b4fa1843..d6472b6d01 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/async_subresource_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/async_subresource_completion.rs @@ -236,6 +236,7 @@ fetch({:?}) body_source_id, network_request_headers: None, head: moli_fetch::ResponseHead { + status_text: None, final_url: request_url, status: 200, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], @@ -450,6 +451,7 @@ async fn streaming_finish_requires_matching_request_and_body_source_identity() { body_source_id, network_request_headers: None, head: moli_fetch::ResponseHead { + status_text: None, final_url: request_url, status: 200, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/extracted/main_module_graphs.rs b/moli-renderer-v8/src/runtime/page_vm/tests/extracted/main_module_graphs.rs index b840317f10..542bf93f03 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/extracted/main_module_graphs.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/extracted/main_module_graphs.rs @@ -624,6 +624,7 @@ fn module_graph_network_result_records_staged_response_started_with_cache_state( let request_url = Url::parse("https://example.com/module.js").expect("request URL"); let response = crate::types::NavigationResponse::from_head_and_text_body( moli_fetch::ResponseHead { + status_text: None, final_url: request_url.clone(), status: 200, headers: vec![("content-type".to_owned(), b"text/javascript".to_vec())], diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch/pending_fetch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch/pending_fetch.rs index 716f12134a..7fa537aee2 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch/pending_fetch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch/pending_fetch.rs @@ -1081,6 +1081,7 @@ impl ScriptVm { // Request-stage fulfillment has no followed redirects yet. Reuse this // complete head for validation and response materialization. let head = moli_fetch::ResponseHead { + status_text: None, final_url: info.url.clone(), status: response_code, headers: response_headers.clone(), @@ -1825,6 +1826,7 @@ impl ScriptVm { WorkerOwnedFetchTarget::from_continuation(&pending.pending.continuation) { let response = response_body.clone_as_navigation_response(moli_fetch::ResponseHead { + status_text: None, final_url: pending.response.final_url.clone(), status: response_code, headers: response_headers.clone(), @@ -1888,6 +1890,7 @@ impl ScriptVm { if let Some(target) = WorkerOwnedXhrTarget::from_continuation(&pending.pending.continuation) { let response = response_body.clone_as_navigation_response(moli_fetch::ResponseHead { + status_text: None, final_url: pending.response.final_url.clone(), status: response_code, headers: response_headers.clone(), @@ -1960,6 +1963,7 @@ impl ScriptVm { None, Ok( response_body.into_navigation_response(moli_fetch::ResponseHead { + status_text: None, final_url: pending.response.final_url, status: response_code, headers: response_headers, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/performance.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/performance.rs index 3d02a27c1c..657826fb8c 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/performance.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/performance.rs @@ -502,6 +502,7 @@ fn parser_script_network_results_populate_buffered_resource_timing_snapshots() { let mut vm = new_storage_test_vm(document_url.as_str()); let response = Ok(crate::types::NavigationResponse::from_head_and_text_body( moli_fetch::ResponseHead { + status_text: None, final_url: script_url.clone(), status: 200, headers: vec![( diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/response_headers.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/response_headers.rs index 00c3d60565..f3eb14527c 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/response_headers.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/response_headers.rs @@ -26,6 +26,7 @@ fn xhr_response_headers_are_filtered_combined_and_sorted_before_events() { assert_eq!(pending.len(), 1); let request = &pending[0]; let head = moli_fetch::ResponseHead { + status_text: None, final_url: request.url.clone(), status: 200, headers: headers.clone(), diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs index c55cd9debe..067b78f50f 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs @@ -939,6 +939,7 @@ fn xml_http_request_blob_response_uses_final_mime_type() { final_url: Url::parse("https://xhr-override-mime.test/mime") .expect("response URL should parse"), status: 200, + status_text: None, headers, request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -1038,6 +1039,7 @@ fn xml_http_request_default_response_type_parses_response_xml_for_document_mime( final_url: Url::parse("https://xhr-response-xml.test/xml-doc") .expect("XML response URL should parse"), status: 200, + status_text: None, headers: vec![("Content-Type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -1064,6 +1066,7 @@ fn xml_http_request_default_response_type_parses_response_xml_for_document_mime( final_url: Url::parse("https://xhr-response-xml.test/plain") .expect("plain response URL should parse"), status: 200, + status_text: None, headers: vec![("Content-Type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -2042,6 +2045,7 @@ __streamingXhr.send(); let response_head = moli_fetch::ResponseHead { final_url: request_url.clone(), status: 200, + status_text: None, headers: vec![ ( "Content-Type".to_owned(), @@ -2204,6 +2208,7 @@ async fn streaming_subresource_finish_preserves_response_head_cache_state() { head: moli_fetch::ResponseHead { final_url: final_url.clone(), status: 200, + status_text: None, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -2497,6 +2502,7 @@ async fn streaming_fetch_body_error_records_response_started_then_body_failed() head: moli_fetch::ResponseHead { final_url: final_url.clone(), status: 206, + status_text: None, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -2596,6 +2602,7 @@ fn install_streaming_fetch_response_fixture( moli_fetch::ResponseHead { final_url: request_url.clone(), status: 200, + status_text: None, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -2655,6 +2662,7 @@ fn install_streaming_fetch_response_fixture( head: moli_fetch::ResponseHead { final_url: request_url, status: 200, + status_text: None, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -2764,6 +2772,7 @@ async fn streaming_fetch_body_cancel_aborts_streaming_subresource() { head: moli_fetch::ResponseHead { final_url: request_url, status: 200, + status_text: None, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -3398,6 +3407,7 @@ async fn streaming_xhr_materialization_failure_errors_body_source_before_close() head: moli_fetch::ResponseHead { final_url: request_url, status: 200, + status_text: None, headers: Vec::new(), request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -3655,6 +3665,7 @@ fn xhr_response_documents_keep_distinct_source_modification_times() { let head = moli_fetch::ResponseHead { final_url: request.url.clone(), status: 200, + status_text: None, headers, request_cookie_report: None, cookie_set_reports: Vec::new(), diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/child_documents.rs b/moli-renderer-v8/src/script_vm/tests/extracted/child_documents.rs index ae907ff2c8..9a62fb919d 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/child_documents.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/child_documents.rs @@ -634,6 +634,7 @@ async fn child_navigation_keeps_accepted_beacon_network_only_and_rejects_stale_s body_source_id, network_request_headers: None, head: moli_fetch::ResponseHead { + status_text: None, final_url: request_url, status: 204, headers: Vec::new(), diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/page_runtime.rs b/moli-renderer-v8/src/script_vm/tests/extracted/page_runtime.rs index 5f0c5a9997..d2d901f1c6 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/page_runtime.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/page_runtime.rs @@ -316,6 +316,7 @@ async fn child_csp_report_keeps_exact_violation_document_without_v8_after_naviga body_source_id, network_request_headers: None, head: moli_fetch::ResponseHead { + status_text: None, final_url: report_url.clone(), status: 204, headers: Vec::new(), diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs index 94a7f49e8e..20970a7a38 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs @@ -15,6 +15,7 @@ fn new_vm_with_pending_response_for_teardown_test() mode: moli_fetch::RequestMode::Cors, }, moli_fetch::ResponseHead { + status_text: None, final_url: document_url.join("data.json").unwrap(), status: 200, headers: vec![("content-type".to_owned(), b"application/json".to_vec())], @@ -1841,6 +1842,7 @@ fn isolated_realm_destruction_aborts_fetch_and_detaches_keepalive() { body_source_id, network_request_headers: None, head: moli_fetch::ResponseHead { + status_text: None, final_url: request_url, status: 200, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index cae5094b7d..9688cf2293 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -359,6 +359,7 @@ fn register_pending_window_fetch_for_test( request_body: None, body_source_id: 10_000 + internal_id, head: moli_fetch::ResponseHead { + status_text: None, final_url: url.clone(), status: 200, headers: Vec::new(), diff --git a/moli-renderer-v8/src/script_vm/tests/post_parse.rs b/moli-renderer-v8/src/script_vm/tests/post_parse.rs index d7f91a23cd..a70158d79e 100644 --- a/moli-renderer-v8/src/script_vm/tests/post_parse.rs +++ b/moli-renderer-v8/src/script_vm/tests/post_parse.rs @@ -291,6 +291,7 @@ fn external_script_redirect_final_url_obeys_script_src_csp() { let final_url = Url::parse("https://cdn.test/final.js").unwrap(); let response = Ok(crate::types::NavigationResponse::from_head_and_text_body( moli_fetch::ResponseHead { + status_text: None, final_url: final_url.clone(), status: 200, headers: Vec::new(), diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_fetch/url_and_initializer_semantics.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch/url_and_initializer_semantics.rs index fb95e80702..898729aff7 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch/url_and_initializer_semantics.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch/url_and_initializer_semantics.rs @@ -717,6 +717,7 @@ fn response_clone_tees_pending_network_body_after_parent_consumption() { moli_fetch::ResponseHead { final_url: response_url, status: 200, + status_text: None, headers: vec![("content-type".to_owned(), b"application/json".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -821,6 +822,7 @@ fn pending_fetch_body_pipe_through_text_decoder_stream_pulls_future_chunks() { moli_fetch::ResponseHead { final_url: response_url, status: 200, + status_text: None, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -942,6 +944,7 @@ fn materialize_response_object_preserves_redirected_slot() { moli_fetch::ResponseHead { final_url: final_url.clone(), status: 200, + status_text: None, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -995,6 +998,7 @@ fn filtered_response_materialization_preserves_urls_across_clone_and_cache() { moli_fetch::ResponseHead { final_url: final_url.clone(), status: 302, + status_text: None, headers: vec![("location".to_owned(), b"target.html".to_vec())], request_cookie_report: None, cookie_set_reports: Vec::new(), @@ -1459,6 +1463,7 @@ fn fetched_null_bodies_discard_payloads_without_registering_pending_streams() { mode: if opaque { moli_fetch::RequestMode::NoCors } else { moli_fetch::RequestMode::Cors }, }; let head = moli_fetch::ResponseHead { + status_text: None, final_url: Url::parse("https://cross-null-response.test/data").unwrap(), status, headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], diff --git a/moli-renderer-v8/src/service_worker_runtime/events.rs b/moli-renderer-v8/src/service_worker_runtime/events.rs index bcd6d66946..8b902e941d 100644 --- a/moli-renderer-v8/src/service_worker_runtime/events.rs +++ b/moli-renderer-v8/src/service_worker_runtime/events.rs @@ -699,6 +699,7 @@ pub(crate) struct MaterializedServiceWorkerFetchResponseHead { pub(crate) response_type: String, pub(crate) redirected: bool, pub(crate) status: u16, + pub(crate) status_text: String, pub(crate) headers: Vec<(String, Vec)>, } diff --git a/moli-renderer-v8/src/service_worker_runtime/script_loading.rs b/moli-renderer-v8/src/service_worker_runtime/script_loading.rs index bd4062d168..2a70b19bad 100644 --- a/moli-renderer-v8/src/service_worker_runtime/script_loading.rs +++ b/moli-renderer-v8/src/service_worker_runtime/script_loading.rs @@ -426,6 +426,7 @@ mod tests { fn script_resource_records_response_metadata_and_body_hash() { let request_url = Url::parse("https://example.test/app/sw.js").unwrap(); let head = ResponseHead { + status_text: None, final_url: Url::parse("https://example.test/app/sw.js?final").unwrap(), status: 200, headers: vec![("Content-Type".to_owned(), b"text/javascript".to_vec())], @@ -814,6 +815,7 @@ mod tests { kind: WorkerScriptResourceKind, ) -> ServiceWorkerScriptResource { let head = ResponseHead { + status_text: None, final_url: script_url.clone(), status: 200, headers: vec![("Content-Type".to_owned(), mime_type.as_bytes().to_vec())], diff --git a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs index f482981abd..d8523c0c5b 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs @@ -63,6 +63,7 @@ fn navigation_preload_response_head( head: moli_fetch::ResponseHead, ) -> MaterializedServiceWorkerFetchResponseHead { MaterializedServiceWorkerFetchResponseHead { + status_text: head.status_text().to_owned(), final_url: Some(head.final_url), response_type: "default".to_owned(), redirected: head.redirected, diff --git a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs index 97098cff5b..80bb6dff39 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs @@ -103,6 +103,7 @@ fn service_worker_fetch_stream_response_head( response_head: &MaterializedServiceWorkerFetchResponseHead, ) -> moli_fetch::ResponseHead { moli_fetch::ResponseHead { + status_text: Some(response_head.status_text.clone()), final_url: response_head .final_url .clone() @@ -601,6 +602,7 @@ impl ServiceWorkerRuntimeService { let response_filter = service_worker_fetch_response_filter(&response); let navigation_response = crate::protocol_types::NavigationResponse::from_head_and_body( moli_fetch::ResponseHead { + status_text: Some(response.status_text.clone()), final_url, status: response.status, headers: response.headers, @@ -1360,6 +1362,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 202, + status_text: "Accepted".to_owned(), headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], }, }); @@ -1444,6 +1447,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], }, }); @@ -3005,6 +3009,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), b"image/png".to_vec())], }, }); @@ -3072,6 +3077,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), b"image/png".to_vec())], }, }); @@ -3136,6 +3142,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), b"image/png".to_vec())], }, }); diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index 35f54c6262..876b582099 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -864,6 +864,9 @@ pub(in crate::worker) fn continue_pending_worker_fetch_response( return; }; if let Some(response_code) = response_code { + if response.head.status != response_code { + response.head.status_text = None; + } response.head.status = response_code; } if let Some(response_headers) = response_headers { @@ -1174,6 +1177,9 @@ pub(in crate::worker) fn continue_pending_worker_xhr_response( return; }; if let Some(response_code) = response_code { + if response.head.status != response_code { + response.head.status_text = None; + } response.head.status = response_code; } if let Some(response_headers) = response_headers { @@ -2502,6 +2508,7 @@ pub(in crate::worker) fn worker_response_from_body( body: RendererSyntheticResponseBody, ) -> Response { body.into_fetch_response(ResponseHead { + status_text: None, final_url, status, headers, diff --git a/moli-renderer-v8/src/worker/thread/dispatch.rs b/moli-renderer-v8/src/worker/thread/dispatch.rs index c72cdf3c32..d8730f40e6 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -3202,6 +3202,7 @@ fn service_worker_fetch_response_head_from_materialized( head: &MaterializedResponseHead, ) -> MaterializedServiceWorkerFetchResponseHead { MaterializedServiceWorkerFetchResponseHead { + status_text: head.status_text.clone(), final_url: head.final_url.clone(), response_type: head.response_type.clone(), redirected: head.redirected, @@ -3393,6 +3394,7 @@ fn start_service_worker_navigation_preload_response_in_context( }; let head = moli_fetch::ResponseHead { + status_text: Some(started.response_head.status_text.clone()), final_url: started .response_head .final_url diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle/worker_and_fetch_lifecycle.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle/worker_and_fetch_lifecycle.rs index f0ad415932..ffab584b4f 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle/worker_and_fetch_lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle/worker_and_fetch_lifecycle.rs @@ -766,6 +766,7 @@ async fn service_worker_fetch_event_preload_response_resolves_network_response() response_type: "default".to_owned(), redirected: false, status: 202, + status_text: "Accepted".to_owned(), headers: vec![("x-preload".to_owned(), b"yes".to_vec())], }, }, @@ -878,6 +879,7 @@ async fn service_worker_fetch_event_preload_response_opaqueredirect_exposes_requ response_type: "default".to_owned(), redirected: false, status: 302, + status_text: "Found".to_owned(), headers: vec![("location".to_owned(), b"/app/final.html".to_vec())], }, }, @@ -1052,6 +1054,7 @@ async fn service_worker_fetch_event_preload_response_body_errors_after_response( response_type: "default".to_owned(), redirected: false, status: 202, + status_text: "Accepted".to_owned(), headers: vec![("x-preload".to_owned(), b"yes".to_vec())], }, }, @@ -1166,6 +1169,7 @@ async fn service_worker_fetch_event_preload_response_body_completes_after_fetch_ response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), b"text/plain".to_vec())], }, },