fix(fetch): preserve redirect mode when filtering responses

This commit is contained in:
ldm0
2026-09-13 17:13:54 +08:00
parent bb19e7a31e
commit 8fa3c7f2d7
18 changed files with 422 additions and 50 deletions
@@ -478,6 +478,7 @@ impl NativeModuleGraphFetchRequest {
&initiator_url,
&head,
request_mode,
moli_fetch::RequestRedirectMode::Follow,
).is_none();
if !crate::subresource_integrity::response_matches_subresource_integrity_metadata(
&body_bytes,
@@ -554,6 +554,7 @@ impl JsContextHost {
credentials_mode: moli_fetch::RequestCredentialsMode,
request_mode: moli_fetch::RequestMode,
request_origin: moli_url::WebOrigin,
redirect_mode: moli_fetch::RequestRedirectMode,
network_partition_key: Option<String>,
policy_context: crate::types::SubresourcePolicyContext,
blob_url_entry: Option<crate::network_host::CapturedBlobUrl>,
@@ -589,6 +590,7 @@ impl JsContextHost {
keepalive,
connect_policy,
csp_report_context,
redirect_mode,
),
),
deferred_request_started: false,
@@ -832,6 +834,7 @@ impl JsContextHost {
credentials_mode: moli_fetch::RequestCredentialsMode,
request_mode: moli_fetch::RequestMode,
request_origin: moli_url::WebOrigin,
redirect_mode: moli_fetch::RequestRedirectMode,
network_partition_key: Option<String>,
policy_context: crate::types::SubresourcePolicyContext,
mut info: PendingSubresourceFetchInfo,
@@ -870,6 +873,7 @@ impl JsContextHost {
keepalive,
connect_policy,
csp_report_context,
redirect_mode,
),
),
deferred_request_started: defer_request_started,
@@ -320,6 +320,7 @@ fn window_fetch_callback_in_relevant_realm<'s>(
FetchResponseRequest {
method: &prepared.method,
mode: prepared.request_mode,
redirect_mode: prepared.redirect_mode,
},
response,
);
@@ -28,6 +28,7 @@ pub(super) fn record_intercepted_fetch(
prepared.credentials_mode,
prepared.request_mode,
prepared.request_origin.clone(),
prepared.redirect_mode,
prepared.network_partition_key,
prepared.policy_context,
prepared.blob_url_entry,
@@ -266,6 +267,7 @@ pub(super) fn spawn_network_fetch(
prepared.credentials_mode,
prepared.request_mode,
prepared.request_origin.clone(),
prepared.redirect_mode,
prepared.network_partition_key.clone(),
prepared.policy_context,
PendingSubresourceFetchInfo {
@@ -64,6 +64,7 @@ pub(super) fn dispatch_service_worker_fetch(
prepared.credentials_mode,
prepared.request_mode,
prepared.request_origin.clone(),
prepared.redirect_mode,
prepared.network_partition_key.clone(),
prepared.policy_context,
PendingSubresourceFetchInfo {
@@ -5,7 +5,7 @@ use super::super::fetch_surface::{
};
use super::*;
use crate::types::NetworkBodySourceId;
use moli_fetch::RequestMode;
use moli_fetch::{RequestMode, RequestRedirectMode};
use moli_url::WebOrigin;
use moli_webapi_declare::WebApiObject;
@@ -13,6 +13,7 @@ use moli_webapi_declare::WebApiObject;
pub(crate) struct FetchResponseRequest<'a> {
pub(crate) method: &'a str,
pub(crate) mode: RequestMode,
pub(crate) redirect_mode: RequestRedirectMode,
}
fn is_redirect_status(status: u16) -> bool {
@@ -83,10 +84,10 @@ struct FetchResponseBodyDeclaration<'scope> {
fn response_filter(
request_origin: impl Into<WebOrigin>,
head: &moli_fetch::ResponseHead,
request_mode: RequestMode,
request: FetchResponseRequest<'_>,
) -> FetchResponseFilter {
let request_origin = request_origin.into();
network_response_filter(&request_origin, head, request_mode)
network_response_filter(&request_origin, head, request.mode, request.redirect_mode)
.map_or(FetchResponseFilter::None, Into::into)
}
@@ -98,11 +99,12 @@ pub(crate) fn network_response_filter(
request_origin: impl Into<WebOrigin>,
head: &moli_fetch::ResponseHead,
request_mode: RequestMode,
redirect_mode: RequestRedirectMode,
) -> Option<crate::types::AsyncSubresourceFetchResponseFilter> {
let request_origin = request_origin.into();
use crate::types::AsyncSubresourceFetchResponseFilter;
if is_redirect_status(head.status) {
if redirect_mode == RequestRedirectMode::Manual && is_redirect_status(head.status) {
Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
} else if request_mode == RequestMode::NoCors
&& head.url_list().has_cross_origin_url(&request_origin)
@@ -214,7 +216,7 @@ pub(crate) fn build_fetch_response_object_from_body_source_for_request_mode_with
let request_origin = request_origin.into();
let filter = filter_override
.map(FetchResponseFilter::from)
.unwrap_or_else(|| response_filter(&request_origin, &head, request.mode));
.unwrap_or_else(|| response_filter(&request_origin, &head, request));
let obj = build_fetch_response_object_head(scope, &request_origin, &head, filter, None);
let body_stream = if response_has_null_body(request.method, head.status) {
None
@@ -235,7 +237,7 @@ pub(crate) fn build_fetch_response_object_from_subresource_body_for_request_mode
body: crate::protocol_types::SubresourceResponseBody,
) -> v8::Local<'s, v8::Object> {
let request_origin = request_origin.into();
let filter = response_filter(&request_origin, &head, request.mode);
let filter = response_filter(&request_origin, &head, request);
let obj = build_fetch_response_object_head(scope, &request_origin, &head, filter, None);
let body_stream = if response_has_null_body(request.method, head.status) {
None
@@ -316,7 +318,7 @@ fn build_fetch_response_object_from_stream_for_request_mode_with_surface_url<'s>
let request_origin = request_origin.into();
let filter = filter_override
.map(FetchResponseFilter::from)
.unwrap_or_else(|| response_filter(&request_origin, &head, request.mode));
.unwrap_or_else(|| response_filter(&request_origin, &head, request));
let filtered_surface_url = (filter == FetchResponseFilter::OpaqueRedirect)
.then_some(filtered_surface_url)
.flatten();
+1 -1
View File
@@ -587,7 +587,7 @@ pub(crate) fn external_script_source_load_outcome_from_response(
let request_mode = external_script_request_mode(script.kind, &script.fetch_metadata);
let head = response.head();
let response_filter =
crate::network_host::network_response_filter(&script.initiator_url, &head, request_mode);
crate::network_host::network_response_filter(&script.initiator_url, &head, request_mode, moli_fetch::RequestRedirectMode::Follow);
let cors_error = if request_mode == RequestMode::Cors {
crate::network_host::validate_cors_response_chain(
&script.initiator_url,
@@ -4794,6 +4794,51 @@ async fn window_fetch_dns_failure_rejects_and_records_network_failure() {
.await;
}
#[tokio::test]
async fn redirect_filter_intercepted_fetch_keeps_redirect_mode_when_resumed() {
run_page_vm_async_test(async move {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
let mut requests = Vec::new();
for _ in 0..7 {
let (mut socket, _) = tokio::time::timeout(Duration::from_secs(5), listener.accept()).await.unwrap().unwrap();
let head = read_http_request_head(&mut socket).await.unwrap();
let path = head.lines().next().unwrap().split_whitespace().nth(1).unwrap();
let status = if path == "/target" { 200 } else { 302 };
let location = if path == "/with-location" { "Location: /target\r\n" } else { "" };
socket.write_all(format!("HTTP/1.1 {status} Test Response\r\n{location}Content-Type: text/plain\r\nContent-Length: 2\r\nCache-Control: no-store\r\nConnection: close\r\n\r\nok").as_bytes()).await.unwrap();
requests.push(path.to_owned());
}
requests
});
let mut page_vm = test_page_vm_with_document_url(Url::parse(&format!("{base}/page")).unwrap());
let local_executor = page_vm.local_executor.clone();
local_executor.run(async move {
page_vm.vm_mut().set_fetch_subresource_interception(true, Some(SubresourceResourceType::Fetch));
for redirect in ["follow", "manual", "error"] {
for path in ["without-location", "with-location"] {
page_vm.vm_mut().eval(&format!(
"globalThis.__resumedRedirect = 'pending'; fetch('/{path}', {{redirect: '{redirect}'}}).then(async response => {{ __resumedRedirect = JSON.stringify([response.type, response.status, await response.text()]); }}, error => {{ __resumedRedirect = error.name; }});"
))?;
let requests = page_vm.vm_mut().take_pending_subresource_fetch_infos();
assert_eq!(requests.len(), 1);
page_vm.continue_pending_subresource_fetch(requests[0].internal_id, None, None, None, None, false, false)?;
drive_websocket_until_done(&mut page_vm, "String(__resumedRedirect !== 'pending')", "resumed fetch should settle").await?;
let expected = match redirect {
"error" => "TypeError".to_owned(),
"manual" => serde_json::json!(["opaqueredirect", 0, ""]).to_string(),
_ => serde_json::json!(["basic", if path == "with-location" { 200 } else { 302 }, "ok"]).to_string(),
};
assert_eq!(page_vm.vm_mut().eval("__resumedRedirect")?, expected, "{redirect}/{path}");
}
}
Ok::<_, anyhow::Error>(())
}).await.unwrap();
assert_eq!(server.await.unwrap(), ["/without-location", "/with-location", "/target", "/without-location", "/with-location", "/without-location", "/with-location"]);
}).await;
}
#[tokio::test]
async fn window_fetch_redirect_error_rejects_before_following_redirect() {
run_page_vm_async_test(async move {
@@ -1420,6 +1420,14 @@ impl ScriptVm {
.with_request_origin(pending.request_origin.clone())
.with_request_mode(pending.request_mode)
.with_use_cors_preflight(pending.continuation.use_cors_preflight())
.with_redirect_mode(
pending
.continuation
.window_fetch()
.map_or(moli_fetch::RequestRedirectMode::Follow, |fetch| {
fetch.redirect_mode()
}),
)
.with_credentials_mode(pending.credentials_mode)
.with_network_partition_key(pending.network_partition_key.clone())
.with_subframe_context(pending.info.frame_id.is_some());
@@ -1693,6 +1701,14 @@ impl ScriptVm {
))
.with_request_mode(pending_fetch.request_mode)
.with_use_cors_preflight(pending_fetch.continuation.use_cors_preflight())
.with_redirect_mode(
pending_fetch
.continuation
.window_fetch()
.map_or(moli_fetch::RequestRedirectMode::Follow, |fetch| {
fetch.redirect_mode()
}),
)
.with_credentials_mode(pending_fetch.credentials_mode)
.with_auth(auth.into())
.with_subframe_context(pending_fetch.info.frame_id.is_some());
@@ -3721,19 +3737,23 @@ impl ScriptVm {
moli_trace::cdp_runtime_trace_enabled().then(Instant::now);
match pending.continuation {
PendingSubresourceContinuation::Fetch(fetch) => {
let redirect_mode = fetch.redirect_mode();
let resolver = fetch
.into_resolver()
.expect("detached keepalive completion is handled before V8 entry");
let resolver = v8::Local::new(scope, &resolver);
let (head, body) = observable_response.into_body();
let (mut head, body) = observable_response.into_body();
if let Some(status_text) = response_status_text {
head.status_text = Some(status_text);
}
let body = if opaque_response_blocked {
moli_fetch::ResponseBody::materialized_bytes(Vec::new())
} else {
body
};
let response_filter = opaque_response_blocked
.then_some(AsyncSubresourceFetchResponseFilter::Opaque)
.or(response_filter);
// ORB discards the internal body; request policy
// still selects opaque versus opaqueredirect.
// Preserve explicit service-worker filter overrides.
let response_obj =
crate::network_host::build_fetch_response_object_from_body_source_for_request_mode_with_filter(
scope,
@@ -3741,19 +3761,12 @@ impl ScriptVm {
crate::network_host::FetchResponseRequest {
method: &response_request_method,
mode: pending.request_mode,
redirect_mode,
},
head,
body,
response_filter,
);
if let Some(status_text) = response_status_text.as_deref() {
crate::network_host::set_response_slot_string(
scope,
response_obj,
crate::network_host::RESPONSE_STATUS_TEXT_SLOT,
status_text,
);
}
resolver.resolve(scope, response_obj.into());
}
PendingSubresourceContinuation::Xhr { xhr, .. } => {
@@ -4965,6 +4978,7 @@ impl ScriptVm {
crate::network_host::FetchResponseRequest {
method: &started.request_method,
mode: pending.request_mode,
redirect_mode: fetch.redirect_mode(),
},
observable_head,
started.body_source_id,
@@ -12,6 +12,7 @@ mod misc;
mod null_body;
mod open_validation;
mod query_realms;
mod redirect_filter;
mod response_type;
mod shadow_dom;
mod streaming_failure;
@@ -0,0 +1,206 @@
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[test]
fn redirect_filter_completion_keeps_status_text_and_explicit_filters() {
use crate::types::AsyncSubresourceFetchResponseFilter::{Opaque, OpaqueRedirect};
for status in [200, 302] {
for redirect in ["follow", "manual"] {
for filter in [None, Some(Opaque), Some(OpaqueRedirect)] {
let mut vm = new_storage_test_vm("https://redirect-filter.test/");
vm.set_fetch_subresource_interception(
true,
Some(crate::types::SubresourceResourceType::Fetch),
);
vm.eval(&format!(
"globalThis.filteredResult = 'pending'; fetch('/response', {{redirect: '{redirect}'}}).then(async response => {{ filteredResult = JSON.stringify([response.type, response.status, response.statusText, response.headers.get('x-visible'), await response.text()]); }});"
)).unwrap();
let requests = vm.take_pending_subresource_fetch_infos();
assert_eq!(requests.len(), 1);
let request = &requests[0];
vm.complete_async_subresource_fetch(
crate::types::AsyncSubresourceFetchCompletion {
internal_id: request.internal_id,
request_url: request.url.clone(),
request_method: request.method.clone(),
request_headers: Vec::new(),
request_body: None,
response_status_text: Some("Override Text".to_owned()),
skip_fetch_security_validation: true,
response_filter: filter,
network_error_text: None,
result: Ok(
crate::protocol_types::NavigationResponse::from_head_and_body(
moli_fetch::ResponseHead {
final_url: request.url.clone(),
status,
status_text: Some("Original Text".to_owned()),
headers: vec![("X-Visible".to_owned(), "present".to_owned())],
request_cookie_report: None,
cookie_set_reports: Vec::new(),
redirected: false,
redirect_chain: Vec::new(),
from_cache: false,
negotiated_http_version: None,
},
"body".to_owned(),
b"body".to_vec(),
),
),
},
)
.unwrap();
let expected_type = match filter {
Some(Opaque) => "opaque",
Some(OpaqueRedirect) => "opaqueredirect",
None if redirect == "manual" && status == 302 => "opaqueredirect",
None => "basic",
};
let filtered = expected_type != "basic";
let expected = serde_json::json!([
expected_type,
if filtered { 0 } else { status },
if filtered { "" } else { "Override Text" },
if filtered { None } else { Some("present") },
if filtered { "" } else { "body" },
]);
assert_eq!(
vm.eval("filteredResult").unwrap(),
expected.to_string(),
"{status}/{redirect}/{filter:?}"
);
}
}
}
}
async fn check_redirect_filter_modes(worker: bool) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let base = format!("http://{address}");
let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel();
let server = tokio::spawn(async move {
let mut requests = 0;
loop {
let mut socket = tokio::select! {
result = listener.accept() => result.unwrap().0,
_ = &mut stop_rx => break,
};
let mut head = Vec::new();
while !head.ends_with(b"\r\n\r\n") {
assert!(head.len() < 8192);
let mut byte = [0];
assert_eq!(socket.read(&mut byte).await.unwrap(), 1);
head.push(byte[0]);
}
let head = String::from_utf8(head).unwrap();
let mut parts = head.lines().next().unwrap().split_whitespace();
let method = parts.next().unwrap();
let path = parts.next().unwrap();
let status: u16 = path.trim_start_matches('/').parse().unwrap();
let response = format!(
"HTTP/1.1 {status} Matrix Response\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\nCache-Control: no-store\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Expose-Headers: X-Visible\r\nX-Visible: present\r\n\r\n{}",
if method == "HEAD" {
""
} else {
"redirect body"
}
);
socket.write_all(response.as_bytes()).await.unwrap();
requests += 1;
}
requests
});
let mut config = moli_fetch::FetchConfig::default();
config.set_http_no_proxy(Some("*".to_owned()));
let loader = ResourceRequestClient::new(&config).unwrap();
let mut vm = new_page_task_executor_test_vm_with_loader(&format!("{base}/page"), &loader);
let probe = format!(
r#"(async () => {{
const base = {base:?};
let count = 0;
for (const status of [200, 301, 302, 303, 307, 308]) {{
for (const redirect of ['follow', 'manual', 'error']) {{
for (const mode of ['cors', 'same-origin', 'no-cors']) {{
for (const remote of [false, true]) {{
for (const method of ['GET', 'HEAD']) {{
for (const override of [false, true]) {{
const label = [status, redirect, mode, remote, method, override].join('/');
const assert = (value, field) => {{ if (!value) throw new Error(label + ': ' + field); }};
const url = new URL('/' + status, base);
if (remote) url.hostname = 'localhost';
const request = new Request(url.href, {{method, mode, redirect: override ? 'follow' : redirect}});
let response, error;
try {{ response = await fetch(request.clone(), override ? {{redirect}} : undefined); }}
catch (value) {{ error = value; }}
count++;
if ((remote && mode === 'same-origin') || (redirect === 'error' && status !== 200)) {{
assert(error instanceof TypeError && !response, 'TypeError rejection');
continue;
}}
assert(!error && response instanceof Response, 'response');
const manual = redirect === 'manual' && status !== 200;
const opaque = !manual && remote && mode === 'no-cors';
const filtered = manual || opaque;
assert(response.type === (manual ? 'opaqueredirect' : opaque ? 'opaque' : remote ? 'cors' : 'basic'), 'type=' + response.type);
assert(response.status === (filtered ? 0 : status), 'status');
assert(response.statusText === (filtered ? '' : 'Matrix Response'), 'statusText');
assert(response.ok === (!filtered && status === 200), 'ok');
assert(!response.redirected, 'redirected without Location');
if (!manual) assert(response.url === (opaque ? '' : url.href), 'url');
assert(response.headers.get('X-Visible') === (filtered ? null : 'present'), 'headers');
if (filtered) assert(response.headers.entries().next().done, 'filtered headers');
assert((response.body === null) === (filtered || method === 'HEAD'), 'body nullability');
const clone = response.clone();
const expected = filtered || method === 'HEAD' ? '' : 'redirect body';
assert(await response.text() === expected, 'body');
assert(await clone.text() === expected, 'clone body');
assert(response.bodyUsed === (!filtered && method !== 'HEAD'), 'bodyUsed');
}}
}}
}}
}}
}}
}}
return String(count);
}})()"#
);
let script = if worker {
let source = format!(
"Promise.resolve().then(() => {probe}).then(value => {{ postMessage(value); close(); }}, error => {{ postMessage(String(error.stack || error)); close(); }});"
);
format!(
"globalThis.redirectFilterResult = 'pending'; const worker = new Worker(URL.createObjectURL(new Blob([{}], {{type: 'text/javascript'}}))); worker.onmessage = event => {{ redirectFilterResult = event.data; }}; worker.onerror = event => {{ redirectFilterResult = event.message; event.preventDefault(); }};",
serde_json::to_string(&source).unwrap()
)
} else {
format!(
"globalThis.redirectFilterResult = 'pending'; Promise.resolve().then(() => {probe}).then(value => {{ redirectFilterResult = value; }}, error => {{ redirectFilterResult = String(error.stack || error); }});"
)
};
vm.eval(&script).unwrap();
tokio::time::timeout(std::time::Duration::from_secs(20), async {
while vm.eval("redirectFilterResult === 'pending'").unwrap() == "true" {
wait_for_one_selected_page_task_executor_test_turn(&mut vm, &loader)
.await
.unwrap();
}
})
.await
.expect("redirect response matrix should finish");
stop_tx.send(()).unwrap();
let requests = server.await.unwrap();
assert_eq!(vm.eval("redirectFilterResult").unwrap(), "432");
// Cross-origin same-origin-mode requests are rejected before transport.
assert_eq!(requests, 360);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn redirect_filter_preserves_window_fetch_modes() {
check_redirect_filter_modes(false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn redirect_filter_preserves_worker_fetch_modes() {
check_redirect_filter_modes(true).await;
}
@@ -222,6 +222,7 @@ fn pending_fetch_continuation<'s>(
)
.expect("test Fetch should capture its CSP report context"),
moli_url::WebOrigin::from_url(host.document_url()),
moli_fetch::RequestRedirectMode::Follow,
),
)
}
@@ -3420,6 +3421,7 @@ fn install_streaming_fetch_response_fixture(
scope,
&document_url,
crate::network_host::FetchResponseRequest {
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
method: "GET",
mode: moli_fetch::RequestMode::Cors,
},
@@ -311,6 +311,7 @@ fn register_pending_window_fetch_for_test(
moli_fetch::RequestCredentialsMode::SameOrigin,
moli_fetch::RequestMode::Cors,
(&url).into(),
moli_fetch::RequestRedirectMode::Follow,
None,
Default::default(),
crate::types::PendingSubresourceFetchInfo {
@@ -475,6 +476,7 @@ fn register_pending_window_fetch_with_connect_policy_for_test(
moli_fetch::RequestCredentialsMode::SameOrigin,
moli_fetch::RequestMode::Cors,
(&document_url).into(),
moli_fetch::RequestRedirectMode::Follow,
None,
Default::default(),
crate::types::PendingSubresourceFetchInfo {
@@ -9217,6 +9217,7 @@ fn fetched_null_bodies_discard_payloads_without_registering_pending_streams() {
for opaque in [false, true] {
for source in ["response", "bytes", "subresource", "stream", "preload"] {
let request = FetchResponseRequest {
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
method,
mode: if opaque { moli_fetch::RequestMode::NoCors } else { moli_fetch::RequestMode::Cors },
};
@@ -9296,6 +9297,7 @@ fn response_clone_tees_pending_network_body_after_parent_consumption() {
scope,
&document_url,
crate::network_host::FetchResponseRequest {
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
method: "GET",
mode: moli_fetch::RequestMode::Cors,
},
@@ -9401,6 +9403,7 @@ fn pending_fetch_body_pipe_through_text_decoder_stream_pulls_future_chunks() {
scope,
&document_url,
crate::network_host::FetchResponseRequest {
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
method: "GET",
mode: moli_fetch::RequestMode::Cors,
},
@@ -9522,6 +9525,7 @@ fn materialize_response_object_preserves_redirected_slot() {
scope,
&document_url,
crate::network_host::FetchResponseRequest {
redirect_mode: moli_fetch::RequestRedirectMode::Follow,
method: "GET",
mode: moli_fetch::RequestMode::Cors,
},
@@ -9574,7 +9578,8 @@ fn filtered_response_materialization_preserves_internal_url_without_exposing_url
let response = crate::network_host::build_fetch_response_object_from_body_source_for_request_mode_with_filter(
scope,
&document_url,
crate::network_host::FetchResponseRequest { method: "GET", mode: moli_fetch::RequestMode::Cors },
crate::network_host::FetchResponseRequest {
redirect_mode: moli_fetch::RequestRedirectMode::Follow, method: "GET", mode: moli_fetch::RequestMode::Cors },
moli_fetch::ResponseHead {
status_text: None,
final_url: final_url.clone(),
@@ -292,6 +292,7 @@ impl ServiceWorkerRuntimeService {
};
let request_mode = job.request.request_mode;
let credentials_mode = job.request.credentials_mode;
let redirect_mode = job.request.redirect_mode;
job.resource_task_runner.spawn(async move {
let result = match request_client
.fetch_raw_stream_with_cancel(request, cancel_handle)
@@ -321,6 +322,7 @@ impl ServiceWorkerRuntimeService {
&request_origin,
&head,
request_mode,
redirect_mode,
);
let body = response.clone_body_bytes();
let navigation_response =
@@ -542,35 +544,32 @@ impl ServiceWorkerRuntimeService {
}
}
if is_redirect_status(response.status)
&& job.request.redirect_mode != moli_fetch::RequestRedirectMode::Manual
&& job.request.redirect_mode == moli_fetch::RequestRedirectMode::Error
{
self.complete_fetch_with_network_failure(
job,
"FetchEvent.respondWith rejected a redirect Response for a request whose redirect mode is error".to_owned(),
crate::network_host::FAILED_ERROR_TEXT.to_owned(),
);
return;
}
if is_redirect_status(response.status)
&& job.request.redirect_mode == moli_fetch::RequestRedirectMode::Follow
{
match service_worker_redirect_target(&job, &response) {
Ok(Some(next_url)) => match job.request.redirect_mode {
moli_fetch::RequestRedirectMode::Error => {
self.complete_fetch_with_network_failure(
job,
format!(
"FetchEvent.respondWith rejected a redirect Response for a request whose redirect mode is error: {next_url}"
),
crate::network_host::FAILED_ERROR_TEXT.to_owned(),
);
Ok(Some(next_url)) => {
if let Err(error) =
apply_service_worker_synthetic_redirect(&mut job, response, next_url)
{
self.complete_fetch_with_failure(job, error);
return;
}
moli_fetch::RequestRedirectMode::Manual => {}
moli_fetch::RequestRedirectMode::Follow => {
if let Err(error) =
apply_service_worker_synthetic_redirect(&mut job, response, next_url)
{
self.complete_fetch_with_failure(job, error);
return;
}
let request = fetch_request_for_job(&job);
if let Err(job) = self.dispatch_controlled_fetch_job(job, request) {
self.dispatch_fetch_fallback(*job);
}
return;
let request = fetch_request_for_job(&job);
if let Err(job) = self.dispatch_controlled_fetch_job(job, request) {
self.dispatch_fetch_fallback(*job);
}
},
return;
}
Ok(None) => {}
Err(error) => {
self.complete_fetch_with_network_failure(
@@ -600,7 +599,7 @@ impl ServiceWorkerRuntimeService {
self.complete_fetch_with_failure(job, message);
return;
}
let response_filter = service_worker_fetch_response_filter(&response);
let response_filter = service_worker_fetch_response_filter(&response, job.redirect_mode);
let navigation_response = crate::protocol_types::NavigationResponse::from_head_and_body(
moli_fetch::ResponseHead {
status_text: Some(response.status_text.clone()),
@@ -881,8 +880,10 @@ fn service_worker_fetch_response_rejection(
fn service_worker_fetch_response_filter(
response: &ServiceWorkerFetchResponse,
redirect_mode: moli_fetch::RequestRedirectMode,
) -> Option<AsyncSubresourceFetchResponseFilter> {
if is_redirect_status(response.status)
if redirect_mode == moli_fetch::RequestRedirectMode::Manual
&& is_redirect_status(response.status)
&& !matches!(response.response_type.as_str(), "opaque" | "opaqueredirect")
{
Some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
@@ -1493,6 +1494,71 @@ mod tests {
}
}
#[test]
fn redirect_filter_service_worker_responses_without_location_preserve_request_mode() {
use moli_fetch::RequestRedirectMode::{Error, Follow, Manual};
for status in [301, 302, 303, 307, 308] {
for mode in [Follow, Manual, Error] {
let service = new_service_worker_runtime_service();
let event_id = ServiceWorkerEventId(1);
let version_id = ServiceWorkerVersionId(1);
let run = RendererServiceWorkerRunIdentity::fresh();
let mut completion_queue =
crate::page_task_queue::RendererResourceCompletionTestHarness::new();
let request_url = insert_active_fetch_job_with_redirect_mode(
&service,
event_id,
version_id,
&run,
1,
completion_queue.sender(),
mode,
);
let headers = vec![("X-Visible".to_owned(), "present".to_owned())];
service.finish_fetch_event_completed(ServiceWorkerFetchCompletion {
event_id,
owner: crate::service_worker_runtime::ServiceWorkerRunOwner::new(
version_id, run,
),
result: ServiceWorkerFetchResult::Response(ServiceWorkerFetchResponse {
final_url: None,
response_type: "default".to_owned(),
redirected: false,
status,
status_text: "No Location".to_owned(),
headers: headers.clone(),
body: b"redirect body".to_vec(),
}),
});
let completion = pop_async_subresource_completion(&mut completion_queue);
if mode == Error {
assert!(
completion
.result
.unwrap_err()
.contains("redirect mode is error")
);
assert_eq!(
completion.network_error_text.as_deref(),
Some(crate::network_host::FAILED_ERROR_TEXT)
);
} else {
assert_eq!(
completion.response_filter,
(mode == Manual)
.then_some(AsyncSubresourceFetchResponseFilter::OpaqueRedirect)
);
let response = completion.result.unwrap();
assert_eq!(response.status, status);
assert_eq!(response.final_url, request_url);
assert_eq!(response.headers, headers);
assert_eq!(response.body_text(), "redirect body");
assert!(!response.redirected);
}
}
}
}
#[test]
fn response_completion_projects_opaque_response_filter_to_subresource_queue() {
let service = new_service_worker_runtime_service();
@@ -1714,7 +1780,7 @@ mod tests {
assert_eq!(
completion.result.err().as_deref(),
Some(
"FetchEvent.respondWith rejected a redirect Response for a request whose redirect mode is error: https://example.test/redirected.txt"
"FetchEvent.respondWith rejected a redirect Response for a request whose redirect mode is error"
)
);
assert_eq!(
+8
View File
@@ -245,6 +245,7 @@ pub(super) struct PendingWindowFetchContinuation {
keepalive: bool,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
csp_report_context: crate::network_host::WindowCspReportRequestContext,
redirect_mode: moli_fetch::RequestRedirectMode,
}
enum PendingWindowFetchPromise {
@@ -258,12 +259,14 @@ impl PendingWindowFetchContinuation {
keepalive: bool,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
csp_report_context: crate::network_host::WindowCspReportRequestContext,
redirect_mode: moli_fetch::RequestRedirectMode,
) -> Self {
Self {
promise: PendingWindowFetchPromise::Active(resolver),
keepalive,
connect_policy,
csp_report_context,
redirect_mode,
}
}
@@ -304,6 +307,11 @@ impl PendingWindowFetchContinuation {
pub(super) fn csp_report_context(&self) -> &crate::network_host::WindowCspReportRequestContext {
&self.csp_report_context
}
pub(super) fn redirect_mode(&self) -> moli_fetch::RequestRedirectMode {
self.redirect_mode
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
@@ -2618,6 +2618,7 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
pending.document_url.clone(),
pending.request_mode,
pending.request_method.clone(),
pending.redirect_mode,
observable_head,
))
}
@@ -2633,8 +2634,14 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
}
return;
}
if let Some((resolver, document_url, request_mode, request_method, observable_head)) =
response_input
if let Some((
resolver,
document_url,
request_mode,
request_method,
redirect_mode,
observable_head,
)) = response_input
{
let response_obj = build_fetch_response_object_from_stream_for_request_mode(
scope,
@@ -2642,6 +2649,7 @@ pub(in crate::worker) fn start_worker_streaming_fetch(
crate::network_host::FetchResponseRequest {
method: &request_method,
mode: request_mode,
redirect_mode,
},
observable_head,
started.body_source_id,
@@ -3034,6 +3042,7 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
crate::network_host::FetchResponseRequest {
method: &pending.request_method,
mode: pending.request_mode,
redirect_mode: pending.redirect_mode,
},
head,
body,
@@ -3052,6 +3061,7 @@ pub(in crate::worker) fn drain_worker_fetch_completion_result(
crate::network_host::FetchResponseRequest {
method: &pending.request_method,
mode: pending.request_mode,
redirect_mode: pending.redirect_mode,
},
head,
body,
@@ -3567,6 +3567,8 @@ fn start_service_worker_navigation_preload_response_in_context(
crate::network_host::FetchResponseRequest {
method: &started.request_method,
mode: started.request_mode,
// Navigation preload always fetches with manual redirects.
redirect_mode: moli_fetch::RequestRedirectMode::Manual,
},
head,
started.body_source_id,