mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 16:01:00 +00:00
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:
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())],
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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())],
|
||||
|
||||
@@ -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())],
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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())],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user