From 3e27fdc93aadef0ccca1fc7ad5b090498faf90db Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 11 Sep 2026 23:49:41 +0800 Subject: [PATCH] fix(fetch): preserve received HTTP status text Carry the wire reason phrase through buffered and streaming transport, response conversion, cache, and Window/Worker Fetch and XHR delivery. Preserve explicitly empty messages, including HTTP/2 and HTTP/3, and Latin-1 reason bytes while retaining existing synthetic defaults. Bump cache metadata to version 4 and keep the original status message when revalidating an entity with a 304 response. Cover transport modes, cache reloads, response conversions, and streaming XHR state changes. --- 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 | 4 +- moli-http-cache/src/tests.rs | 48 ++--- moli-page-types/src/lib.rs | 42 +++++ moli-protocol/src/conn/fetch_support.rs | 2 + .../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/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/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 + .../src/runtime/page_vm/tests/mod.rs | 1 + .../src/script_vm/subresource_fetch.rs | 4 + .../script_vm/tests/browser_api/navigation.rs | 1 + .../src/script_vm/tests/dom_xhr/xhr.rs | 24 ++- moli-renderer-v8/src/script_vm/tests/mod.rs | 4 + .../src/script_vm/tests/post_parse.rs | 1 + .../src/script_vm/tests/webidl_fetch.rs | 6 + .../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 + .../src/worker/thread/tests/lifecycle.rs | 4 + 49 files changed, 504 insertions(+), 81 deletions(-) diff --git a/moli-core/src/runtime/tests.rs b/moli-core/src/runtime/tests.rs index cca4f7c238..f648a2003a 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(), "text/html".to_owned())], diff --git a/moli-fetch/src/blocking/cache/lookup.rs b/moli-fetch/src/blocking/cache/lookup.rs index 18f4116f52..18cc061395 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, String)], 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 ee67bda583..325f4bf371 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, String)], 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, String)]) -> bool { diff --git a/moli-fetch/src/blocking/cache/revalidation.rs b/moli-fetch/src/blocking/cache/revalidation.rs index d188ad5287..ce770f5267 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 0146248662..4d38d7be48 100644 --- a/moli-fetch/src/blocking/collectors.rs +++ b/moli-fetch/src/blocking/collectors.rs @@ -14,6 +14,7 @@ use url::Url; use crate::{ FetchCancelHandle, FetchConfig, NegotiatedHttpVersion, NetworkRequestExtraInfo, Request, client_hints::{ClientHintResponseAction, ClientHintResponsePolicy}, + response::parse_http_response_status_line, }; use crate::RedirectInfo; @@ -121,6 +122,7 @@ pub(crate) fn log_request_completion( pub(crate) struct ResponseCollector { body: Vec, headers: Vec<(String, String)>, + status_text: Option, max_response_size: Option, response_too_large: bool, cancel_handle: Option, @@ -137,10 +139,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, String)] { &self.headers } @@ -154,6 +161,7 @@ impl ResponseCollector { pub struct StreamingResponseCollector { cookie_store: SharedBrowserCookieStore, headers: Vec<(String, String)>, + status_text: Option, current_url: Option, current_cookie_context: Option, status: u16, @@ -222,6 +230,7 @@ impl StreamingCachePlan { pub struct RawStreamingResponseCollector { cookie_store: SharedBrowserCookieStore, headers: Vec<(String, String)>, + status_text: Option, current_url: Option, current_cookie_context: Option, status: u16, @@ -261,6 +270,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, @@ -295,6 +305,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; @@ -341,6 +352,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, String)] { &self.headers } @@ -486,6 +501,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(), @@ -616,6 +632,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, @@ -650,6 +667,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; @@ -699,6 +717,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, String)] { &self.headers } @@ -849,6 +871,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(), @@ -952,8 +975,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; } @@ -1010,14 +1034,11 @@ 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.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(); @@ -1083,14 +1104,11 @@ 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.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(); @@ -1149,6 +1167,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 09b056992c..21e8167428 100644 --- a/moli-fetch/src/blocking/mod.rs +++ b/moli-fetch/src/blocking/mod.rs @@ -64,6 +64,7 @@ pub(crate) enum RequestHttpVersion { pub struct StreamingHtmlResponseStart { pub final_url: Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, String)>, pub request_cookie_report: Option, pub cookie_set_reports: Vec, @@ -79,6 +80,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 d0ea33fa86..1fab0b949d 100644 --- a/moli-fetch/src/proxy_connect.rs +++ b/moli-fetch/src/proxy_connect.rs @@ -3,6 +3,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, String)>, } @@ -11,6 +12,7 @@ pub(crate) struct ProxyConnectResponseRecorder { enabled: bool, collecting: bool, pending_status: Option, + pending_status_text: String, pending_headers: Vec<(String, String)>, observed_bytes: usize, completed: Option, @@ -21,6 +23,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; @@ -34,6 +37,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; @@ -48,14 +52,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 = String::from_utf8_lossy(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; } @@ -66,6 +72,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; @@ -94,14 +101,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() -} - fn parse_header_line(line: &str) -> Option<(String, String)> { let (name, value) = line.split_once(':')?; let name = name.trim(); @@ -139,6 +138,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(), "Basic realm=\"proxy\"".to_owned(), diff --git a/moli-fetch/src/response.rs b/moli-fetch/src/response.rs index 05656a27f3..f5cfa01b7c 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, String)>, 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, String)>, 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, String)>, 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 a44ea631cb..3c7e414fda 100644 --- a/moli-fetch/src/runtime.rs +++ b/moli-fetch/src/runtime.rs @@ -1768,13 +1768,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(), @@ -1819,6 +1820,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -1881,6 +1883,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -2079,13 +2082,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(), @@ -2201,6 +2205,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -2268,6 +2273,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -2277,6 +2283,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, @@ -2311,6 +2318,7 @@ impl RuntimeOwner { request_cookie_header.as_deref(), &final_url, status, + status_text.as_deref(), &headers, false, cache_body_writer, @@ -2835,6 +2843,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, @@ -2863,6 +2872,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, @@ -2886,6 +2896,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, @@ -2983,6 +2994,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, @@ -3006,6 +3018,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, @@ -3449,6 +3462,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(), @@ -3541,6 +3555,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); @@ -3548,6 +3563,7 @@ fn collect_buffered_response( Ok(( RawResponse::from_head_and_body( ResponseHead { + status_text, final_url, status, headers, @@ -4207,6 +4223,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(), "text/html".to_owned())], diff --git a/moli-fetch/src/streaming_response.rs b/moli-fetch/src/streaming_response.rs index 10abf7a395..77f8b8377f 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, String)>, 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, String)>, 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(), "text/plain".to_owned())], diff --git a/moli-fetch/src/tests/mod.rs b/moli-fetch/src/tests/mod.rs index 79e2be65e8..154147bdde 100644 --- a/moli-fetch/src/tests/mod.rs +++ b/moli-fetch/src/tests/mod.rs @@ -61,6 +61,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(), "text/plain".to_owned())], @@ -112,22 +113,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] @@ -4397,7 +4524,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") @@ -4416,6 +4544,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, @@ -5319,6 +5454,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) @@ -5361,6 +5498,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 d72f43f448..60fe4f426b 100644 --- a/moli-http-cache/src/metadata.rs +++ b/moli-http-cache/src/metadata.rs @@ -11,7 +11,7 @@ use typed_num::Num; use crate::{path_safety::safe_body_file_name, time::unique_suffix}; pub(crate) const META_FILE: &str = "meta.json"; -pub(crate) type HttpCacheFormatVersion = Num<3>; +pub(crate) type HttpCacheFormatVersion = Num<4>; /// Request header value captured for a `Vary` response header. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -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, String)>, 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 71af8715fb..2eb0286a79 100644 --- a/moli-http-cache/src/tests.rs +++ b/moli-http-cache/src/tests.rs @@ -672,6 +672,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, @@ -720,29 +721,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(()) @@ -794,6 +797,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 691f1330c7..066081a8ee 100644 --- a/moli-page-types/src/lib.rs +++ b/moli-page-types/src/lib.rs @@ -316,6 +316,7 @@ impl From for RedirectInfo { pub struct NavigationResponse { pub final_url: Url, pub status: u16, + pub status_text: Option, pub headers: Vec<(String, String)>, body: ResponseBody, pub request_cookie_report: Option, @@ -332,6 +333,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 @@ -377,6 +379,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(), @@ -421,6 +424,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, @@ -445,6 +449,7 @@ impl NavigationResponse { ) -> Self { Self::from_head_and_text_body( ResponseHead { + status_text: None, final_url, status, headers, @@ -465,6 +470,9 @@ impl NavigationResponse { headers: Vec<(String, String)>, ) -> 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()) @@ -475,6 +483,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, @@ -511,6 +520,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, @@ -3505,6 +3515,36 @@ mod tests { Url::parse(&format!("https://example.test{path}")).expect("test URL should parse") } + #[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(), "true".to_owned())]; + 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 = @@ -3975,6 +4015,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(), @@ -4001,6 +4042,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(), diff --git a/moli-protocol/src/conn/fetch_support.rs b/moli-protocol/src/conn/fetch_support.rs index b2f7c41431..5ee00bf9ee 100644 --- a/moli-protocol/src/conn/fetch_support.rs +++ b/moli-protocol/src/conn/fetch_support.rs @@ -483,6 +483,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![( @@ -1293,6 +1294,7 @@ impl ActiveDocumentBodyStreamState { fn finish_pending_body_source(&mut self) -> Result { let head = ResponseHead { + status_text: self.response.status_text.clone(), final_url: self.response.final_url.clone(), status: self.response.status, headers: self.response.headers.clone(), diff --git a/moli-protocol/src/conn/page_state/fetch_state.rs b/moli-protocol/src/conn/page_state/fetch_state.rs index 4c70c5507b..8f7b3df56a 100644 --- a/moli-protocol/src/conn/page_state/fetch_state.rs +++ b/moli-protocol/src/conn/page_state/fetch_state.rs @@ -380,6 +380,7 @@ mod tests { request_headers: Vec::new(), response: RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: url.clone(), status: 200, headers: vec![("content-type".to_owned(), "text/html".to_owned())], diff --git a/moli-protocol/src/conn/runtime_load.rs b/moli-protocol/src/conn/runtime_load.rs index d3692de7b9..74ffceee52 100644 --- a/moli-protocol/src/conn/runtime_load.rs +++ b/moli-protocol/src/conn/runtime_load.rs @@ -98,6 +98,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![( @@ -1050,6 +1051,7 @@ fn decoded_data_url_navigation_response( Url::parse(raw_url).map_err(|error| format!("failed to parse data url: {error}"))?; let response = RawResponse::from_head_and_body( ResponseHead { + status_text: None, final_url: requested_url.clone(), status: 200, headers: vec![("Content-Type".to_owned(), decoded.content_type)], @@ -1141,6 +1143,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, @@ -2828,6 +2831,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 ea45988928..ebf4975ee7 100644 --- a/moli-protocol/src/domains/fetch/navigation.rs +++ b/moli-protocol/src/domains/fetch/navigation.rs @@ -650,6 +650,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![("Content-Type".to_owned(), content_type)], diff --git a/moli-protocol/src/domains/network/tests/cache.rs b/moli-protocol/src/domains/network/tests/cache.rs index cf65aae467..8743a8721c 100644 --- a/moli-protocol/src/domains/network/tests/cache.rs +++ b/moli-protocol/src/domains/network/tests/cache.rs @@ -673,6 +673,7 @@ async fn clear_browser_cache_keeps_pending_response_navigation_transfer() { request_headers: Vec::new(), 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 130f11bfa5..04e7285f5f 100644 --- a/moli-renderer-v8/src/document_script_scheduler.rs +++ b/moli-renderer-v8/src/document_script_scheduler.rs @@ -1099,6 +1099,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(), "text/javascript".to_owned())], 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 4edec71993..40787e06e4 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 @@ -904,6 +904,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(), "text/html".to_owned())], 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 b06b7327b6..e489c29211 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 @@ -2854,6 +2854,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(), @@ -2898,6 +2899,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 ec4bfaff98..d441321726 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(), "max-age=60".to_owned())], @@ -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(), "max-age=60".to_owned())], @@ -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(), "max-age=60".to_owned())], diff --git a/moli-renderer-v8/src/network/request_client/tests.rs b/moli-renderer-v8/src/network/request_client/tests.rs index c64fbe1fa0..ac6cfddb1b 100644 --- a/moli-renderer-v8/src/network/request_client/tests.rs +++ b/moli-renderer-v8/src/network/request_client/tests.rs @@ -172,6 +172,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(), "max-age=60".to_owned())], @@ -1467,6 +1468,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(), "text/plain".to_owned())], diff --git a/moli-renderer-v8/src/network_host.rs b/moli-renderer-v8/src/network_host.rs index 4da17479f2..f78188184d 100644 --- a/moli-renderer-v8/src/network_host.rs +++ b/moli-renderer-v8/src/network_host.rs @@ -22,7 +22,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; @@ -67,7 +66,6 @@ pub(crate) use self::body_source::{ error_pending_network_body_stream_with_reason, new_network_body_source_id, pending_network_body_stream, release_pending_opaque_response_body, }; -pub(in crate::network_host) use self::browser_response::http_status_text; pub(crate) use self::browser_response::{ local_url_response, local_url_response_result, local_url_response_with_blob_entry, }; diff --git a/moli-renderer-v8/src/network_host/async_fetch.rs b/moli-renderer-v8/src/network_host/async_fetch.rs index 6d98354317..a03adbad9b 100644 --- a/moli-renderer-v8/src/network_host/async_fetch.rs +++ b/moli-renderer-v8/src/network_host/async_fetch.rs @@ -884,6 +884,7 @@ mod tests { let transition = redirects .advance( ResponseHead { + status_text: None, final_url: Url::parse("https://origin.test/start")?, status: 303, headers: vec![( diff --git a/moli-renderer-v8/src/network_host/browser_response.rs b/moli-renderer-v8/src/network_host/browser_response.rs index 331f0101c0..cdc4ddffc2 100644 --- a/moli-renderer-v8/src/network_host/browser_response.rs +++ b/moli-renderer-v8/src/network_host/browser_response.rs @@ -1,13 +1,6 @@ use super::*; use moli_web_mime::data_url_body_and_mime_type; -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())?; Some(blob_response(url, body_bytes, mime_type)) @@ -20,6 +13,7 @@ pub(super) fn blob_response(url: &url::Url, body_bytes: Vec, mime_type: Stri } Response::from_head_and_lossy_body_bytes( moli_fetch::ResponseHead { + status_text: None, final_url: url.clone(), status: 200, headers, @@ -38,6 +32,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![("Content-Type".to_owned(), mime_type)], diff --git a/moli-renderer-v8/src/network_host/response/materialize.rs b/moli-renderer-v8/src/network_host/response/materialize.rs index 9dbfd7f471..6c17859d12 100644 --- a/moli-renderer-v8/src/network_host/response/materialize.rs +++ b/moli-renderer-v8/src/network_host/response/materialize.rs @@ -166,9 +166,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 { "" } @@ -382,7 +382,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(&head.headers, HeadersGuard::Response); let internal_headers_obj = diff --git a/moli-renderer-v8/src/network_host/xhr.rs b/moli-renderer-v8/src/network_host/xhr.rs index f40b48c861..31e8ba92e9 100644 --- a/moli-renderer-v8/src/network_host/xhr.rs +++ b/moli-renderer-v8/src/network_host/xhr.rs @@ -37,6 +37,7 @@ pub(crate) const XHR_SEND_FLAG_SLOT: &str = "__lmXhrSendFlag"; pub(crate) const XHR_UPLOAD_IN_PROGRESS_SLOT: &str = "__lmXhrUploadInProgress"; 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 f4bbd70e4f..7f8043d187 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 0c7fd77bf9..9653627608 100644 --- a/moli-renderer-v8/src/network_host/xhr/delivery/response.rs +++ b/moli-renderer-v8/src/network_host/xhr/delivery/response.rs @@ -417,7 +417,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 = xhr_response_url(head); set_xhr_state_string(scope, xhr, XHR_RESPONSE_URL_SLOT, response_url.as_str()); 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 2dbed91d31..59c7bb1e9d 100644 --- a/moli-renderer-v8/src/network_host/xhr/instance_state.rs +++ b/moli-renderer-v8/src/network_host/xhr/instance_state.rs @@ -87,6 +87,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 448a50366b..dd43ce1ede 100644 --- a/moli-renderer-v8/src/runtime/browser_context_runtime.rs +++ b/moli-renderer-v8/src/runtime/browser_context_runtime.rs @@ -267,6 +267,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 74ad7a85a0..af3f50d6ae 100644 --- a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs +++ b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs @@ -481,6 +481,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 53a0de6893..c3c5fbc5ce 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(), "text/plain".to_owned())], @@ -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(), "text/plain".to_owned())], diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index e792821134..fec42e8381 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -14203,6 +14203,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(), "text/javascript".to_owned())], diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch.rs index 9d26183ca2..638d55fdc6 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch.rs @@ -2148,6 +2148,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(), @@ -2887,6 +2888,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(), @@ -2949,6 +2951,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(), @@ -3020,6 +3023,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.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs index b95a4a6c58..64fbf8c796 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs @@ -778,6 +778,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/xhr.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs index f73bb984dc..d2c71f0c84 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 @@ -936,6 +936,7 @@ fn xml_http_request_override_mime_type_affects_response_mime() { scope, xhr, moli_fetch::ResponseHead { + status_text: None, final_url: Url::parse("https://xhr-override-mime.test/mime") .expect("response URL should parse"), status: 200, @@ -1015,6 +1016,7 @@ fn xml_http_request_default_response_type_parses_response_xml_for_document_mime( scope, xml_xhr, moli_fetch::ResponseHead { + status_text: None, final_url: Url::parse("https://xhr-response-xml.test/xml-doc") .expect("XML response URL should parse"), status: 200, @@ -1041,6 +1043,7 @@ fn xml_http_request_default_response_type_parses_response_xml_for_document_mime( scope, plain_xhr, moli_fetch::ResponseHead { + status_text: None, final_url: Url::parse("https://xhr-response-xml.test/plain") .expect("plain response URL should parse"), status: 200, @@ -1114,6 +1117,7 @@ fn xml_http_request_response_document_uses_response_url_and_requester_origin() { scope, xhr, moli_fetch::ResponseHead { + status_text: None, final_url: Url::parse("https://response.example/resource/doc#fragment").unwrap(), status: 200, headers: vec![("Content-Type".to_owned(), mime.to_owned())], @@ -1204,6 +1208,7 @@ fn xml_http_request_document_response_requires_an_eligible_mime_and_well_formed_ 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: mime @@ -2377,6 +2382,7 @@ __streamingXhr.send(); let request_url = pending.url.clone(); let body_source_id = crate::network_host::new_network_body_source_id(); let response_head = moli_fetch::ResponseHead { + status_text: Some("Streamed message".to_owned()), final_url: request_url.clone(), status: 200, headers: vec![ @@ -2413,12 +2419,13 @@ __streamingXhr.send(); events: __streamingXhrEvents, readyState: __streamingXhr.readyState, status: __streamingXhr.status, + statusText: __streamingXhr.statusText, responseText: __streamingXhr.responseText, contentType: __streamingXhr.getResponseHeader("content-type") })"#, ) .expect("XHR response head should be Web-visible"), - r#"{"events":["readystatechange:1:0:","readystatechange:2:200:"],"readyState":2,"status":200,"responseText":"","contentType":"text/plain; charset=utf-8"}"# + r#"{"events":["readystatechange:1:0:","readystatechange:2:200:"],"readyState":2,"status":200,"statusText":"Streamed message","responseText":"","contentType":"text/plain; charset=utf-8"}"# ); vm.append_streaming_async_subresource_fetch_chunk(body_source_id, b"hi \xe2".to_vec()); @@ -2452,6 +2459,15 @@ __streamingXhr.send(); r#"[4,200,"hi €!",["readystatechange:3:200:hi €!","progress:7:7:true","readystatechange:4:200:hi €!","load","loadend"]]"#, "DONE must flush the latest deferred progress before readystatechange 4" ); + assert_eq!( + vm.eval("__streamingXhr.statusText").unwrap(), + "Streamed message" + ); + assert_eq!( + vm.eval("__streamingXhr.open('GET', '/next'); __streamingXhr.statusText") + .unwrap(), + "" + ); } #[tokio::test(flavor = "current_thread")] @@ -2538,6 +2554,7 @@ async fn streaming_subresource_finish_preserves_response_head_cache_state() { request_body: None, body_source_id, head: moli_fetch::ResponseHead { + status_text: None, final_url: final_url.clone(), status: 200, headers: vec![("content-type".to_owned(), "text/plain".to_owned())], @@ -2828,6 +2845,7 @@ async fn streaming_fetch_body_error_records_response_started_then_body_failed() request_body: None, body_source_id, head: moli_fetch::ResponseHead { + status_text: None, final_url: final_url.clone(), status: 206, headers: vec![("content-type".to_owned(), "text/plain".to_owned())], @@ -2927,6 +2945,7 @@ fn install_streaming_fetch_response_fixture( mode: moli_fetch::RequestMode::Cors, }, moli_fetch::ResponseHead { + status_text: None, final_url: request_url.clone(), status: 200, headers: vec![("content-type".to_owned(), "text/plain".to_owned())], @@ -2986,6 +3005,7 @@ fn install_streaming_fetch_response_fixture( request_body: None, body_source_id, head: moli_fetch::ResponseHead { + status_text: None, final_url: request_url, status: 200, headers: vec![("content-type".to_owned(), "text/plain".to_owned())], @@ -3095,6 +3115,7 @@ async fn streaming_fetch_body_cancel_aborts_streaming_subresource() { request_body: None, body_source_id, head: moli_fetch::ResponseHead { + status_text: None, final_url: request_url, status: 200, headers: vec![("content-type".to_owned(), "text/plain".to_owned())], @@ -3726,6 +3747,7 @@ async fn streaming_xhr_materialization_failure_errors_body_source_before_close() request_body: None, body_source_id, head: moli_fetch::ResponseHead { + status_text: None, final_url: request_url, status: 200, headers: Vec::new(), diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 9df986fb1b..cf7ec8eba3 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -358,6 +358,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(), @@ -3367,6 +3368,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(), @@ -3610,6 +3612,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(), @@ -3811,6 +3814,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(), "text/plain".to_owned())], 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 68196d4779..a3d0d905a1 100644 --- a/moli-renderer-v8/src/script_vm/tests/post_parse.rs +++ b/moli-renderer-v8/src/script_vm/tests/post_parse.rs @@ -348,6 +348,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.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs index f4b2f5dd44..ade2469fd4 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs @@ -9471,6 +9471,7 @@ fn opaque_window_fetch_keeps_blocked_bytes_out_of_internal_clone_consumers() { body_source_id: id, network_request_headers: None, head: moli_fetch::ResponseHead { + status_text: None, final_url: pending.url.clone(), status: 200, headers: vec![("Content-Type".to_owned(), mime.to_owned())], @@ -9568,6 +9569,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(), "text/plain".to_owned())], @@ -9646,6 +9648,7 @@ fn response_clone_tees_pending_network_body_after_parent_consumption() { mode: moli_fetch::RequestMode::Cors, }, moli_fetch::ResponseHead { + status_text: None, final_url: response_url, status: 200, headers: vec![("content-type".to_owned(), "application/json".to_owned())], @@ -9750,6 +9753,7 @@ fn pending_fetch_body_pipe_through_text_decoder_stream_pulls_future_chunks() { mode: moli_fetch::RequestMode::Cors, }, moli_fetch::ResponseHead { + status_text: None, final_url: response_url, status: 200, headers: vec![("content-type".to_owned(), "text/plain".to_owned())], @@ -9871,6 +9875,7 @@ fn materialize_response_object_preserves_redirected_slot() { }, moli_fetch::Response::from_head_and_text_body( moli_fetch::ResponseHead { + status_text: None, final_url: final_url.clone(), status: 200, headers: vec![("content-type".to_owned(), "text/plain".to_owned())], @@ -9919,6 +9924,7 @@ fn filtered_response_materialization_preserves_internal_url_without_exposing_url &document_url, crate::network_host::FetchResponseRequest { method: "GET", mode: moli_fetch::RequestMode::Cors }, moli_fetch::ResponseHead { + status_text: None, final_url: final_url.clone(), status: 302, headers: vec![("location".to_owned(), "target.html".to_owned())], diff --git a/moli-renderer-v8/src/service_worker_runtime/events.rs b/moli-renderer-v8/src/service_worker_runtime/events.rs index 28fa344931..b8cbd5f5d3 100644 --- a/moli-renderer-v8/src/service_worker_runtime/events.rs +++ b/moli-renderer-v8/src/service_worker_runtime/events.rs @@ -695,6 +695,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, String)>, } 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 1def35185a..8040303d7e 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(), "text/javascript".to_owned())], @@ -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.to_owned())], 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 5252095cee..94a03c6a7a 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 @@ -61,6 +61,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 899dbf087f..def516fbe1 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 @@ -102,6 +102,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, @@ -1318,6 +1320,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 202, + status_text: "Accepted".to_owned(), headers: vec![("content-type".to_owned(), "text/plain".to_owned())], }, }); @@ -1402,6 +1405,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), "text/plain".to_owned())], }, }); @@ -2963,6 +2967,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), "image/png".to_owned())], }, }); @@ -3030,6 +3035,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), "image/png".to_owned())], }, }); @@ -3094,6 +3100,7 @@ mod tests { response_type: "default".to_owned(), redirected: false, status: 200, + status_text: "OK".to_owned(), headers: vec![("content-type".to_owned(), "image/png".to_owned())], }, }); diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index 0aac517faf..f24769f41c 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -823,6 +823,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 { @@ -1098,6 +1101,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 { @@ -2458,6 +2464,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 edd554a322..ed376f73f9 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -3354,6 +3354,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, @@ -3545,6 +3546,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.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs index fc5df55d48..df4af3e268 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs @@ -768,6 +768,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(), "yes".to_owned())], }, }, @@ -880,6 +881,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(), "/app/final.html".to_owned())], }, }, @@ -1054,6 +1056,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(), "yes".to_owned())], }, }, @@ -1168,6 +1171,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(), "text/plain".to_owned())], }, },