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.
This commit is contained in:
ldm0
2026-09-16 22:12:23 +08:00
parent 9ae1769276
commit 3e27fdc93a
49 changed files with 504 additions and 81 deletions
+1
View File
@@ -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())],
+3
View File
@@ -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 {
+5 -2
View File
@@ -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<HttpCacheEntryMetadata> {
@@ -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 {
+1
View File
@@ -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,
) {
+51 -15
View File
@@ -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<u8>,
headers: Vec<(String, String)>,
status_text: Option<String>,
max_response_size: Option<usize>,
response_too_large: bool,
cancel_handle: Option<FetchCancelHandle>,
@@ -137,10 +139,15 @@ impl ResponseCollector {
pub(crate) fn begin_request(&mut self, max_response_size: Option<usize>) {
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<String>,
current_url: Option<Url>,
current_cookie_context: Option<NetworkCookieRequestContext>,
status: u16,
@@ -222,6 +230,7 @@ impl StreamingCachePlan {
pub struct RawStreamingResponseCollector {
cookie_store: SharedBrowserCookieStore,
headers: Vec<(String, String)>,
status_text: Option<String>,
current_url: Option<Url>,
current_cookie_context: Option<NetworkCookieRequestContext>,
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<HttpCacheBodyWriter>,
) {
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<RedirectInfo>,
) {
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::<u16>().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::<u16>().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: </early.css>; 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],
+2
View File
@@ -64,6 +64,7 @@ pub(crate) enum RequestHttpVersion {
pub struct StreamingHtmlResponseStart {
pub final_url: Url,
pub status: u16,
pub status_text: Option<String>,
pub headers: Vec<(String, String)>,
pub request_cookie_report: Option<StoredCookieQueryReport>,
pub cookie_set_reports: Vec<StoredCookieSetReport>,
@@ -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,
+10 -10
View File
@@ -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<u16>,
pending_status_text: String,
pending_headers: Vec<(String, String)>,
observed_bytes: usize,
completed: Option<ProxyConnectResponse>,
@@ -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<u16> {
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(),
+77
View File
@@ -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<String>,
pub headers: Vec<(String, String)>,
pub request_cookie_report: Option<StoredCookieQueryReport>,
pub cookie_set_reports: Vec<StoredCookieSetReport>,
@@ -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<NegotiatedHttpVersion>,
}
pub(crate) fn parse_http_response_status_line(data: &[u8]) -> Option<HttpResponseStatusLine> {
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::<String>()
.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<String>,
pub headers: Vec<(String, String)>,
body: ResponseBody,
pub request_cookie_report: Option<StoredCookieQueryReport>,
@@ -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<String>,
pub headers: Vec<(String, String)>,
body: ResponseBody,
pub request_cookie_report: Option<StoredCookieQueryReport>,
@@ -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,
+19 -2
View File
@@ -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())],
+9
View File
@@ -24,6 +24,7 @@ impl fmt::Debug for StreamingResponseLifetimeLease {
pub struct StreamingHtmlResponse {
pub final_url: Url,
pub status: u16,
pub status_text: Option<String>,
pub headers: Vec<(String, String)>,
pub request_cookie_report: Option<StoredCookieQueryReport>,
pub cookie_set_reports: Vec<StoredCookieSetReport>,
@@ -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<String>,
pub headers: Vec<(String, String)>,
pub request_cookie_report: Option<StoredCookieQueryReport>,
pub cookie_set_reports: Vec<StoredCookieSetReport>,
@@ -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())],
+154 -15
View File
@@ -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)
+3 -1
View File
@@ -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<String>,
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,
+26 -22
View File
@@ -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,
+42
View File
@@ -316,6 +316,7 @@ impl From<NavigationRedirect> for RedirectInfo {
pub struct NavigationResponse {
pub final_url: Url,
pub status: u16,
pub status_text: Option<String>,
pub headers: Vec<(String, String)>,
body: ResponseBody,
pub request_cookie_report: Option<StoredCookieQueryReport>,
@@ -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(),
+2
View File
@@ -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<DocumentBodySource, String> {
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(),
@@ -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())],
+4
View File
@@ -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,
@@ -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)],
@@ -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(),
@@ -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())],
@@ -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())],
@@ -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(),
@@ -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())],
@@ -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())],
-2
View File
@@ -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,
};
@@ -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![(
@@ -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<Response> {
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<u8>, 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<Response> {
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)],
@@ -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 =
+1
View File
@@ -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";
@@ -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);
@@ -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());
@@ -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)]
@@ -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,
@@ -481,6 +481,7 @@ fn about_blank_navigation_response(url: &Url) -> Option<moli_fetch::Response> {
}
Some(moli_fetch::Response::from_head_and_lossy_body_bytes(
moli_fetch::ResponseHead {
status_text: None,
final_url: url.clone(),
status: 200,
headers: vec![(
@@ -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())],
@@ -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())],
@@ -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,
@@ -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![(
@@ -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(),
@@ -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())],
@@ -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(),
@@ -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())],
@@ -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)>,
}
@@ -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())],
@@ -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,
@@ -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())],
},
});
@@ -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,
@@ -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
@@ -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())],
},
},