From 8fa3c7f2d7ea49133eecce71b5f21691feac4681 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 12 Sep 2026 23:30:42 +0800 Subject: [PATCH] fix(fetch): preserve redirect mode when filtering responses --- moli-renderer-v8/src/module_runtime/graph.rs | 1 + .../context_host/resource_loading.rs | 4 + .../src/network_host/fetch/bindings.rs | 1 + .../src/network_host/fetch/bindings/paths.rs | 2 + .../fetch/bindings/service_worker.rs | 1 + .../src/network_host/response/materialize.rs | 16 +- moli-renderer-v8/src/planning.rs | 2 +- .../src/runtime/page_vm/tests/fetch_xhr.rs | 45 ++++ .../src/script_vm/subresource_fetch.rs | 38 +++- .../src/script_vm/tests/dom_xhr/mod.rs | 1 + .../tests/dom_xhr/redirect_filter.rs | 206 ++++++++++++++++++ .../src/script_vm/tests/dom_xhr/xhr.rs | 2 + moli-renderer-v8/src/script_vm/tests/mod.rs | 2 + .../src/script_vm/tests/webidl_fetch.rs | 7 +- .../service/fetch_settlement.rs | 120 +++++++--- moli-renderer-v8/src/types.rs | 8 + .../src/worker/global_scope/fetch.rs | 14 +- .../src/worker/thread/dispatch.rs | 2 + 18 files changed, 422 insertions(+), 50 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/dom_xhr/redirect_filter.rs diff --git a/moli-renderer-v8/src/module_runtime/graph.rs b/moli-renderer-v8/src/module_runtime/graph.rs index d082d74c57..4e06c0a371 100644 --- a/moli-renderer-v8/src/module_runtime/graph.rs +++ b/moli-renderer-v8/src/module_runtime/graph.rs @@ -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, diff --git a/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs b/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs index ce71204249..6df390e65f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/resource_loading.rs @@ -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, policy_context: crate::types::SubresourcePolicyContext, blob_url_entry: Option, @@ -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, 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, diff --git a/moli-renderer-v8/src/network_host/fetch/bindings.rs b/moli-renderer-v8/src/network_host/fetch/bindings.rs index fbe697d34d..853ea36ed1 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings.rs @@ -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, ); diff --git a/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs b/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs index 70a41f4ed6..2b9d3ba34f 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs @@ -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 { diff --git a/moli-renderer-v8/src/network_host/fetch/bindings/service_worker.rs b/moli-renderer-v8/src/network_host/fetch/bindings/service_worker.rs index cf3b7006e8..17d1ce9e9a 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings/service_worker.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings/service_worker.rs @@ -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 { diff --git a/moli-renderer-v8/src/network_host/response/materialize.rs b/moli-renderer-v8/src/network_host/response/materialize.rs index fcc8c8db75..15993555fa 100644 --- a/moli-renderer-v8/src/network_host/response/materialize.rs +++ b/moli-renderer-v8/src/network_host/response/materialize.rs @@ -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, 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, head: &moli_fetch::ResponseHead, request_mode: RequestMode, + redirect_mode: RequestRedirectMode, ) -> Option { 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(); diff --git a/moli-renderer-v8/src/planning.rs b/moli-renderer-v8/src/planning.rs index 845ddaebdf..2621ee0928 100644 --- a/moli-renderer-v8/src/planning.rs +++ b/moli-renderer-v8/src/planning.rs @@ -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, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/fetch_xhr.rs b/moli-renderer-v8/src/runtime/page_vm/tests/fetch_xhr.rs index 8219980aa0..0905a96131 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/fetch_xhr.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/fetch_xhr.rs @@ -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 { diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch.rs index fa08abaf25..df79414674 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch.rs @@ -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, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/mod.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/mod.rs index 1388796cfe..1cf07a6549 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/mod.rs @@ -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; diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/redirect_filter.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/redirect_filter.rs new file mode 100644 index 0000000000..f2205360af --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/redirect_filter.rs @@ -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; +} diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs index a7344f05ab..43e3d09fcf 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs @@ -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, }, diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 703e7e0241..23ee52f79e 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -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 { diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs index ac5b15f8b4..1442754393 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs @@ -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(), diff --git a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs index 849c715d0f..5a4b277fea 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs @@ -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 { - 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!( diff --git a/moli-renderer-v8/src/types.rs b/moli-renderer-v8/src/types.rs index d23b6d0b6b..713b939cd6 100644 --- a/moli-renderer-v8/src/types.rs +++ b/moli-renderer-v8/src/types.rs @@ -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)] diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index f6895263d6..d1cb4c12f6 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -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, diff --git a/moli-renderer-v8/src/worker/thread/dispatch.rs b/moli-renderer-v8/src/worker/thread/dispatch.rs index ed376f73f9..63eee97f7e 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -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,