fix(fetch): preserve received HTTP status text

Carry received HTTP reason phrases from the parser through buffered and streaming responses, disk and memory caches, page responses, Fetch, and XHR. Preserve custom and empty HTTP/1 phrases and report empty text for HTTP/2 and HTTP/3. Synthesized responses retain canonical fallback. Regressions cover whitespace, non-ASCII bytes, and cache round trips.
This commit is contained in:
ldm0
2026-10-11 21:19:08 +08:00
committed by Donough Liu
parent e806cfbeb3
commit cfc97efe51
55 changed files with 497 additions and 79 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(), b"text/html".to_vec())],
+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, Vec<u8>)],
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, Vec<u8>)],
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, Vec<u8>)]) -> 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
@@ -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<u8>,
headers: Vec<(String, Vec<u8>)>,
status_text: Option<String>,
max_response_size: Option<usize>,
response_too_large: bool,
cancel_handle: Option<FetchCancelHandle>,
@@ -142,10 +144,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, Vec<u8>)] {
&self.headers
}
@@ -159,6 +166,7 @@ impl ResponseCollector {
pub struct StreamingResponseCollector {
cookie_store: SharedBrowserCookieStore,
headers: Vec<(String, Vec<u8>)>,
status_text: Option<String>,
current_url: Option<Url>,
current_cookie_context: Option<NetworkCookieRequestContext>,
status: u16,
@@ -228,6 +236,7 @@ impl StreamingCachePlan {
pub struct RawStreamingResponseCollector {
cookie_store: SharedBrowserCookieStore,
headers: Vec<(String, Vec<u8>)>,
status_text: Option<String>,
current_url: Option<Url>,
current_cookie_context: Option<NetworkCookieRequestContext>,
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<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;
@@ -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<u8>)] {
&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<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;
@@ -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<u8>)] {
&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::<u16>().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::<u16>().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: </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
@@ -66,6 +66,7 @@ pub(crate) enum RequestHttpVersion {
pub struct StreamingHtmlResponseStart {
pub final_url: Url,
pub status: u16,
pub status_text: Option<String>,
pub headers: Vec<(String, Vec<u8>)>,
pub request_cookie_report: Option<StoredCookieQueryReport>,
pub cookie_set_reports: Vec<StoredCookieSetReport>,
@@ -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,
+10 -10
View File
@@ -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<u8>)>,
}
@@ -13,6 +14,7 @@ pub(crate) struct ProxyConnectResponseRecorder {
enabled: bool,
collecting: bool,
pending_status: Option<u16>,
pending_status_text: String,
pending_headers: Vec<(String, Vec<u8>)>,
observed_bytes: usize,
completed: Option<ProxyConnectResponse>,
@@ -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<u16> {
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(),
+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, Vec<u8>)>,
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, Vec<u8>)>,
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, Vec<u8>)>,
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
@@ -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())],
+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, Vec<u8>)>,
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, Vec<u8>)>,
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(), b"text/plain".to_vec())],
+154 -15
View File
@@ -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)
+2
View File
@@ -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, Vec<u8>)>,
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
@@ -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,
+43
View File
@@ -318,6 +318,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, Vec<u8>)>,
body: ResponseBody,
pub request_cookie_report: Option<StoredCookieQueryReport>,
@@ -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<u8>)>,
) -> 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(),
+1
View File
@@ -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![(
@@ -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())],
+4
View File
@@ -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,
@@ -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![(
@@ -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(),
@@ -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())],
@@ -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())],
@@ -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(),
@@ -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())],
@@ -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<String>,
pub headers: Vec<(String, Vec<u8>)>,
pub body: Vec<u8>,
pub completion_error: Option<String>,
@@ -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(),
@@ -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())],
-2
View File
@@ -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,
};
@@ -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())],
@@ -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<Response> {
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<Response> {
.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<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![(
@@ -566,6 +566,7 @@ mod tests {
headers: Vec<(String, Vec<u8>)>,
) -> 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())],
@@ -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),
+1
View File
@@ -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";
@@ -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);
@@ -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
@@ -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)]
@@ -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,
@@ -480,6 +480,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(), 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())],
@@ -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())],
@@ -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,
@@ -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![(
@@ -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(),
@@ -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(),
@@ -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(),
@@ -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(),
@@ -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())],
@@ -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(),
@@ -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(),
@@ -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())],
@@ -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<u8>)>,
}
@@ -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())],
@@ -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,
@@ -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())],
},
});
@@ -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,
@@ -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
@@ -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())],
},
},