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())], }, },