diff --git a/moli-file-api/src/blob_store.rs b/moli-file-api/src/blob_store.rs index cef89a0c8e..4c86d6325c 100644 --- a/moli-file-api/src/blob_store.rs +++ b/moli-file-api/src/blob_store.rs @@ -207,15 +207,19 @@ where /// Return object URL bytes and MIME type, excluding its fragment. pub fn object_url_bytes_and_type(&self, url: &str) -> Option<(Vec, String)> { + let (bytes, mime_type) = self.object_url_shared_bytes_and_type(url)?; + Some((bytes.to_vec(), mime_type)) + } + + /// Capture an object URL entry without copying its immutable body. The + /// captured entry remains usable after revocation or creator teardown. + pub fn object_url_shared_bytes_and_type(&self, url: &str) -> Option<(Arc<[u8]>, String)> { let url = url.split_once('#').map_or(url, |(url, _)| url); - let blob_id = self - .object_urls - .lock() - .get(url) - .map(|state| state.blob_id)?; - let bytes = self.blob_bytes(blob_id)?; - let mime_type = self.blob_mime_type(blob_id).unwrap_or_default(); - Some((bytes, mime_type)) + let object_urls = self.object_urls.lock(); + let blob_id = object_urls.get(url)?.blob_id; + let blobs = self.blobs.lock(); + let blob = blobs.by_id.get(&blob_id)?; + Some((blob.bytes.clone(), blob.mime_type.clone())) } /// Return object URL body decoded lossily as text plus MIME type. @@ -348,6 +352,47 @@ fn random_uuid() -> String { mod tests { use super::*; + #[test] + fn captured_object_url_entries_outlive_revocation_and_creator_cleanup() { + for cleanup in ["revoke", "owner", "lifetime"] { + let store = BlobStore::::default(); + let blob = store.create_blob( + Some(1), + Some(10), + vec![0, 128, 255], + "application/example".to_owned(), + ); + let url = store + .create_object_url_with_lifetime(Some(1), Some(101), blob, "https://example.test") + .unwrap(); + let entry = store + .object_url_shared_bytes_and_type(&format!("{url}#fragment")) + .unwrap(); + let clone = store.object_url_shared_bytes_and_type(&url).unwrap(); + assert!(Arc::ptr_eq(&entry.0, &clone.0)); + let weak = Arc::downgrade(&entry.0); + store.release_blob_wrapper_ref(blob); + match cleanup { + "revoke" => { + assert!(store.revoke_object_url(&url)); + } + "owner" => store.cleanup_owner_resources(1), + "lifetime" => { + assert_eq!(store.cleanup_object_url_lifetime(1, 101), 1); + } + _ => unreachable!(), + } + assert!(store.object_url_shared_bytes_and_type(&url).is_none()); + assert!(store.blob_bytes(blob).is_none()); + assert_eq!(&*entry.0, &[0, 128, 255]); + assert_eq!(entry.1, "application/example"); + drop(entry); + assert!(weak.upgrade().is_some()); + drop(clone); + assert!(weak.upgrade().is_none()); + } + } + #[test] fn object_url_retains_blob_until_revoked() { let store = BlobStore::::default(); diff --git a/moli-renderer-v8/src/blob.rs b/moli-renderer-v8/src/blob.rs index 6baf8c877e..6f52565527 100644 --- a/moli-renderer-v8/src/blob.rs +++ b/moli-renderer-v8/src/blob.rs @@ -411,6 +411,10 @@ pub(super) fn object_url_bytes_and_type(url: &str) -> Option<(Vec, String)> blob_store().object_url_bytes_and_type(url) } +pub(super) fn object_url_shared_bytes_and_type(url: &str) -> Option<(Arc<[u8]>, String)> { + blob_store().object_url_shared_bytes_and_type(url) +} + pub(super) fn collect_blob_bytes_and_type<'s>( scope: &mut v8::PinScope<'s, '_>, parts_value: v8::Local<'s, v8::Value>, 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 f62560d0e4..b06b7327b6 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 @@ -558,6 +558,7 @@ impl JsContextHost { request_origin: moli_url::WebOrigin, network_partition_key: Option, policy_context: crate::types::SubresourcePolicyContext, + blob_url_entry: Option, mut info: PendingSubresourceFetchInfo, ) { self.assign_pending_subresource_fetch_identity(&mut info); @@ -593,6 +594,7 @@ impl JsContextHost { ), ), deferred_request_started: false, + blob_url_entry, }, ); self.note_subresource_activity(); @@ -630,6 +632,7 @@ impl JsContextHost { fetch_id, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -663,6 +666,7 @@ impl JsContextHost { policy_context: Default::default(), continuation: PendingSubresourceContinuation::WorkerXhr { worker_id, xhr_id }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -700,6 +704,7 @@ impl JsContextHost { report_id, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -737,6 +742,7 @@ impl JsContextHost { fetch_id, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -773,6 +779,7 @@ impl JsContextHost { xhr_id, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -810,6 +817,7 @@ impl JsContextHost { report_id, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -867,6 +875,7 @@ impl JsContextHost { ), ), deferred_request_started: defer_request_started, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -914,6 +923,7 @@ impl JsContextHost { policy_context, continuation: PendingSubresourceContinuation::EventSource(event_source), deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -970,6 +980,7 @@ impl JsContextHost { sequence, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1041,6 +1052,7 @@ impl JsContextHost { request_initiator_type, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1156,6 +1168,7 @@ impl JsContextHost { sequence, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1212,6 +1225,7 @@ impl JsContextHost { css_image, }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1249,6 +1263,7 @@ impl JsContextHost { policy_context: Default::default(), continuation: PendingSubresourceContinuation::Beacon, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1289,6 +1304,7 @@ impl JsContextHost { policy_context, continuation: PendingSubresourceContinuation::CspReport { client_id }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1325,6 +1341,7 @@ impl JsContextHost { policy_context: Default::default(), continuation: PendingSubresourceContinuation::Beacon, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1365,6 +1382,7 @@ impl JsContextHost { policy_context, continuation: PendingSubresourceContinuation::CspReport { client_id }, deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1378,6 +1396,7 @@ impl JsContextHost { credentials_mode: moli_fetch::RequestCredentialsMode, network_partition_key: Option, policy_context: crate::types::SubresourcePolicyContext, + blob_url_entry: Option, mut info: PendingSubresourceFetchInfo, ) -> u64 { self.assign_pending_subresource_fetch_identity(&mut info); @@ -1402,6 +1421,7 @@ impl JsContextHost { policy_context, continuation: PendingSubresourceContinuation::Xhr(xhr), deferred_request_started: false, + blob_url_entry, }, ); self.note_subresource_activity(); @@ -1437,6 +1457,7 @@ impl JsContextHost { policy_context: Default::default(), continuation: PendingSubresourceContinuation::WebSocket(connection), deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); @@ -1475,6 +1496,7 @@ impl JsContextHost { policy_context, continuation: PendingSubresourceContinuation::Xhr(xhr), deferred_request_started: false, + blob_url_entry: None, }, ); self.note_subresource_activity(); diff --git a/moli-renderer-v8/src/network_host.rs b/moli-renderer-v8/src/network_host.rs index 6b0490cc94..b84cd9187d 100644 --- a/moli-renderer-v8/src/network_host.rs +++ b/moli-renderer-v8/src/network_host.rs @@ -1,6 +1,7 @@ mod async_fetch; mod beacon; mod bindings; +mod blob_url_entry; mod body; mod body_source; mod browser_response; @@ -27,6 +28,9 @@ use moli_webapi_declare::WebApiObject; use crate::network::ResourceRequestClient; +pub(in crate::network_host) use self::blob_url_entry::BLOB_URL_ENTRY_SLOT; +pub(crate) use self::blob_url_entry::{CapturedBlobUrl, blob_url_entry, set_blob_url_entry}; + pub(crate) use self::async_fetch::{ browser_request_needs_manual_preflight_redirects, fetch_browser_subresource_raw_stream_with_preflight_headers_and_network_metadata, @@ -64,7 +68,9 @@ pub(crate) use self::body_source::{ 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}; +pub(crate) use self::browser_response::{ + local_url_response, local_url_response_result, local_url_response_with_blob_entry, +}; pub(crate) use self::csp_reports::{ WindowCspReportRequestContext, capture_window_csp_report_request_context, send_content_security_policy_reports_for_lightweight_popup, diff --git a/moli-renderer-v8/src/network_host/blob_url_entry.rs b/moli-renderer-v8/src/network_host/blob_url_entry.rs new file mode 100644 index 0000000000..fb3e23a2df --- /dev/null +++ b/moli-renderer-v8/src/network_host/blob_url_entry.rs @@ -0,0 +1,217 @@ +//! URL parsing captures the blob entry, including a failed lookup. Revoking +//! the URL must not invalidate an existing Request or a successfully opened +//! XHR. Private carriers retain shared bytes until their last JS owner is +//! collected; the isolate store also releases them on isolate teardown. + +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; + +use crate::util::{get_private_value, set_private_value}; + +pub(in crate::network_host) const BLOB_URL_ENTRY_SLOT: &str = "__lmBlobUrlEntry"; +const ID_SLOT: &str = "__lmBlobUrlEntryId"; + +#[derive(Clone)] +pub(crate) struct CapturedBlobUrl { + url: url::Url, + data: Option<(Arc<[u8]>, String)>, +} + +impl CapturedBlobUrl { + pub(crate) fn capture(url: &url::Url) -> Option { + if url.scheme() != "blob" { + return None; + } + let mut url = url.clone(); + url.set_fragment(None); + let data = crate::blob::object_url_shared_bytes_and_type(url.as_str()); + Some(Self { url, data }) + } + + pub(super) fn response(&self, url: &url::Url) -> Option { + let (body, mime_type) = self.data.as_ref()?; + Some(super::browser_response::blob_response( + url, + body.to_vec(), + mime_type.clone(), + )) + } + + pub(super) fn matches(&self, url: &url::Url) -> bool { + self.url.as_str() + == url + .as_str() + .split_once('#') + .map_or(url.as_str(), |(url, _)| url) + } +} + +type Store = Rc>; + +#[derive(Default)] +struct Entries { + next_id: u64, + values: HashMap, CapturedBlobUrl)>, +} + +pub(crate) fn set_blob_url_entry<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + entry: Option, +) { + let Some(entry) = entry else { + set_private_value( + scope, + owner, + BLOB_URL_ENTRY_SLOT, + v8::undefined(scope).into(), + ); + return; + }; + let store = if let Some(store) = scope.get_slot::() { + store.clone() + } else { + let store = Store::default(); + scope.set_slot(store.clone()); + store + }; + let id = { + let mut store = store.borrow_mut(); + store.next_id = store + .next_id + .checked_add(1) + .expect("blob entry identity exhausted"); + store.next_id + }; + let carrier = v8::Object::new(scope); + let weak_store = Rc::downgrade(&store); + let weak = v8::Weak::with_finalizer( + scope, + carrier, + Box::new(move |_| { + if let Some(store) = weak_store.upgrade() { + store.borrow_mut().values.remove(&id); + } + }), + ); + store.borrow_mut().values.insert(id, (weak, entry)); + set_private_value( + scope, + carrier, + ID_SLOT, + v8::BigInt::new_from_u64(scope, id).into(), + ); + set_private_value(scope, owner, BLOB_URL_ENTRY_SLOT, carrier.into()); +} + +pub(crate) fn blob_url_entry<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, +) -> Option { + let carrier = get_private_value(scope, owner, BLOB_URL_ENTRY_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let id = get_private_value(scope, carrier, ID_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())? + .u64_value() + .0; + scope + .get_slot::()? + .borrow() + .values + .get(&id) + .map(|(_, entry)| entry.clone()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn entry() -> CapturedBlobUrl { + CapturedBlobUrl { + url: url::Url::parse("blob:https://example.test/captured").unwrap(), + data: Some((Arc::from([0, 128, 255]), "application/example".to_owned())), + } + } + + #[test] + fn private_entries_follow_the_last_owner_across_realms_gc_and_isolate_drop() { + moli_v8_test_util::ensure_v8(); + let weak = { + let mut isolate = v8::Isolate::new(Default::default()); + let (weak, surviving_owner) = { + let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let first = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, first); + let owner = v8::Object::new(scope); + let entry = entry(); + let weak = Arc::downgrade(&entry.data.as_ref().unwrap().0); + set_blob_url_entry(scope, owner, Some(entry)); + let second = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, second); + let other = v8::Object::new(scope); + let carrier = get_private_value(scope, owner, BLOB_URL_ENTRY_SLOT).unwrap(); + set_private_value(scope, other, BLOB_URL_ENTRY_SLOT, carrier); + let captured = blob_url_entry(scope, other).unwrap(); + assert!(Arc::ptr_eq( + &captured.data.unwrap().0, + &weak.upgrade().unwrap() + )); + (weak, v8::Global::new(scope, other)) + }; + isolate.low_memory_notification(); + assert!(weak.upgrade().is_some()); + drop(surviving_owner); + isolate.low_memory_notification(); + assert!(weak.upgrade().is_none()); + assert!( + isolate + .get_slot::() + .unwrap() + .borrow() + .values + .is_empty() + ); + + let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + let owner = v8::Object::new(scope); + let entry = entry(); + let weak = Arc::downgrade(&entry.data.as_ref().unwrap().0); + set_blob_url_entry(scope, owner, Some(entry)); + weak + }; + assert!(weak.upgrade().is_none()); + } + + #[test] + fn captured_entries_match_fragments_but_do_not_override_other_urls_or_methods() { + let captured = entry(); + let mut url = captured.url.clone(); + url.set_fragment(Some("fragment")); + let response = + super::super::local_url_response_with_blob_entry(&url, "GET", Some(&captured)) + .unwrap() + .unwrap(); + assert_eq!(response.head().status, 200); + let (_, body) = response.into_body(); + assert_eq!( + body.try_into_materialized_bytes().unwrap(), + vec![0, 128, 255] + ); + assert!( + super::super::local_url_response_with_blob_entry(&url, "POST", Some(&captured)) + .unwrap() + .is_err() + ); + let other = url::Url::parse("blob:https://example.test/other").unwrap(); + assert!( + super::super::local_url_response_with_blob_entry(&other, "GET", Some(&captured)) + .unwrap() + .is_err() + ); + let missing = CapturedBlobUrl { url, data: None }; + assert!(missing.response(&missing.url).is_none()); + } +} diff --git a/moli-renderer-v8/src/network_host/browser_response.rs b/moli-renderer-v8/src/network_host/browser_response.rs index 151521872b..331f0101c0 100644 --- a/moli-renderer-v8/src/network_host/browser_response.rs +++ b/moli-renderer-v8/src/network_host/browser_response.rs @@ -10,11 +10,15 @@ pub(in crate::network_host) fn http_status_text(status: u16) -> &'static str { pub(crate) fn blob_url_response(url: &url::Url) -> Option { let (body_bytes, mime_type) = blob::object_url_bytes_and_type(url.as_str())?; + Some(blob_response(url, body_bytes, mime_type)) +} + +pub(super) fn blob_response(url: &url::Url, body_bytes: Vec, mime_type: String) -> Response { let mut headers = Vec::new(); if !mime_type.is_empty() { headers.push(("Content-Type".to_owned(), mime_type)); } - Some(Response::from_head_and_lossy_body_bytes( + Response::from_head_and_lossy_body_bytes( moli_fetch::ResponseHead { final_url: url.clone(), status: 200, @@ -27,7 +31,7 @@ pub(crate) fn blob_url_response(url: &url::Url) -> Option { negotiated_http_version: None, }, body_bytes, - )) + ) } pub(crate) fn data_url_response(url: &url::Url) -> Option { @@ -63,13 +67,25 @@ pub(crate) fn local_url_response(url: &url::Url) -> Option { pub(crate) fn local_url_response_result( url: &url::Url, method: &str, +) -> Option> { + local_url_response_with_blob_entry(url, method, None) +} + +pub(crate) fn local_url_response_with_blob_entry( + url: &url::Url, + method: &str, + entry: Option<&CapturedBlobUrl>, ) -> Option> { match url.scheme() { "blob" if method != "GET" => { Some(Err(format!("blob URL fetch requires GET, got `{method}`"))) } "blob" => { - Some(blob_url_response(url).ok_or_else(|| format!("blob URL `{url}` is unavailable"))) + let response = match entry.filter(|entry| entry.matches(url)) { + Some(entry) => entry.response(url), + None => blob_url_response(url), + }; + Some(response.ok_or_else(|| format!("blob URL `{url}` is unavailable"))) } "data" => { Some(data_url_response(url).ok_or_else(|| format!("data URL `{url}` is invalid"))) 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 6d0184a943..6acdb1f61c 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings/paths.rs @@ -30,6 +30,7 @@ pub(super) fn record_intercepted_fetch( prepared.request_origin.clone(), prepared.network_partition_key, prepared.policy_context, + prepared.blob_url_entry, PendingSubresourceFetchInfo { internal_id: 0, network_request_handle: None, @@ -144,7 +145,11 @@ pub(super) fn resolve_local_fetch( host: &mut JsContextHost, prepared: &PreparedWindowFetchRequest, ) -> Result, String> { - let Some(result) = local_url_response_result(&prepared.resolved_url, &prepared.method) else { + let Some(result) = local_url_response_with_blob_entry( + &prepared.resolved_url, + &prepared.method, + prepared.blob_url_entry.as_ref(), + ) else { return Ok(None); }; let response = result.map_err(|message| { diff --git a/moli-renderer-v8/src/network_host/fetch/bindings/request.rs b/moli-renderer-v8/src/network_host/fetch/bindings/request.rs index 2a76b3260b..2136c3d192 100644 --- a/moli-renderer-v8/src/network_host/fetch/bindings/request.rs +++ b/moli-renderer-v8/src/network_host/fetch/bindings/request.rs @@ -13,6 +13,7 @@ pub(super) struct PreparedWindowFetchRequest { pub(super) document_referrer_policy: Option, pub(super) policy_context: crate::types::SubresourcePolicyContext, pub(super) resolved_url: url::Url, + pub(super) blob_url_entry: Option, pub(super) method: String, pub(super) request_headers: Vec<(String, String)>, pub(super) cors_preflight_request_headers: Vec<(String, String)>, @@ -82,6 +83,9 @@ pub(super) fn prepare_window_fetch_request<'s>( .validate(scope, parsed.request_mode.as_ref(), &parsed.cache)? .unwrap_or(parsed.referrer); + let blob_url_entry = parsed + .blob_url_entry + .or_else(|| CapturedBlobUrl::capture(&resolved_url)); Ok(PreparedWindowFetchRequest { frame_id, fetch_context, @@ -94,6 +98,7 @@ pub(super) fn prepare_window_fetch_request<'s>( document_referrer_policy, policy_context, resolved_url, + blob_url_entry, method: parsed.method, request_headers, cors_preflight_request_headers, diff --git a/moli-renderer-v8/src/network_host/fetch/input.rs b/moli-renderer-v8/src/network_host/fetch/input.rs index 98b9c64459..3ae17d8ff2 100644 --- a/moli-renderer-v8/src/network_host/fetch/input.rs +++ b/moli-renderer-v8/src/network_host/fetch/input.rs @@ -4,6 +4,7 @@ use std::str::FromStr; pub(super) struct ParsedWindowFetchInput { pub(super) url: String, + pub(super) blob_url_entry: Option, pub(super) method: String, pub(super) body: Option>, pub(super) body_stream: Option>, @@ -95,6 +96,7 @@ pub(super) fn parse_window_fetch_input<'s>( }); Ok(ParsedWindowFetchInput { url, + blob_url_entry: inherited.blob_url_entry, method, body, body_stream, @@ -138,6 +140,7 @@ pub(super) fn parse_window_fetch_input<'s>( .unwrap_or(moli_fetch::RequestRedirectMode::Follow); Ok(ParsedWindowFetchInput { url, + blob_url_entry: None, method: init.method, body: init.body, body_stream: init.body_stream, diff --git a/moli-renderer-v8/src/network_host/fetch_surface.rs b/moli-renderer-v8/src/network_host/fetch_surface.rs index bd6f25f667..db308e64c0 100644 --- a/moli-renderer-v8/src/network_host/fetch_surface.rs +++ b/moli-renderer-v8/src/network_host/fetch_surface.rs @@ -817,6 +817,7 @@ fn request_clone_callback<'s>( .bind(scope) .expect("Request clone shell declaration should bind"); for slot in [ + BLOB_URL_ENTRY_SLOT, REQUEST_METHOD_SLOT, REQUEST_URL_SLOT, REQUEST_DESTINATION_SLOT, diff --git a/moli-renderer-v8/src/network_host/request/bindings.rs b/moli-renderer-v8/src/network_host/request/bindings.rs index 76741b5c48..bc593f0acc 100644 --- a/moli-renderer-v8/src/network_host/request/bindings.rs +++ b/moli-renderer-v8/src/network_host/request/bindings.rs @@ -90,6 +90,14 @@ pub(crate) fn request_constructor_callback<'s>( return; } + // Fresh URL entry lookup follows RequestInit conversion. Inherited URL + // entries, including unavailable entries, survive getters that revoke it. + let blob_url_entry = state.blob_url_entry.or_else(|| { + url::Url::parse(&state.url_resolved) + .ok() + .and_then(|url| CapturedBlobUrl::capture(&url)) + }); + set_blob_url_entry(scope, obj, blob_url_entry); append_default_body_content_type(&mut state.headers, state.body_content_type.as_deref()); let body_buffer = state .body diff --git a/moli-renderer-v8/src/network_host/request/bindings/init.rs b/moli-renderer-v8/src/network_host/request/bindings/init.rs index ce0afb5a15..1b3c3184ff 100644 --- a/moli-renderer-v8/src/network_host/request/bindings/init.rs +++ b/moli-renderer-v8/src/network_host/request/bindings/init.rs @@ -12,6 +12,7 @@ use moli_fetch::RequestRedirectMode; pub(super) struct RequestConstructionState { pub(super) url_resolved: String, + pub(super) blob_url_entry: Option, pub(super) method: String, pub(super) mode: String, pub(super) cache: String, @@ -121,10 +122,14 @@ pub(super) fn request_initial_state<'s>( .as_ref() .map(|snapshot| snapshot.headers.clone()) .unwrap_or_default(); + let blob_url_entry = inherited + .as_ref() + .and_then(|snapshot| snapshot.blob_url_entry.clone()); let signal = inherited.and_then(|snapshot| snapshot.signal); Ok(RequestConstructionState { url_resolved, + blob_url_entry, method, mode, cache, diff --git a/moli-renderer-v8/src/network_host/request/input.rs b/moli-renderer-v8/src/network_host/request/input.rs index 3222069162..9fbb9de82b 100644 --- a/moli-renderer-v8/src/network_host/request/input.rs +++ b/moli-renderer-v8/src/network_host/request/input.rs @@ -64,6 +64,7 @@ pub(super) fn request_headers_guard_for_mode(mode: &str) -> HeadersGuard { pub(crate) struct RequestInputSnapshot { pub(crate) url: String, + pub(crate) blob_url_entry: Option, pub(crate) method: String, pub(crate) mode: String, pub(crate) cache: String, @@ -175,6 +176,7 @@ fn request_input_snapshot_inner<'s>( let signal = request_signal_snapshot_from_property(scope, object)?; Ok(Some(RequestInputSnapshot { url, + blob_url_entry: None, method, mode, cache, @@ -251,6 +253,7 @@ fn request_input_snapshot_from_private_slots<'s>( .flatten(); Ok(RequestInputSnapshot { url, + blob_url_entry: blob_url_entry(scope, object), method, mode, cache, diff --git a/moli-renderer-v8/src/network_host/xhr/bindings/open.rs b/moli-renderer-v8/src/network_host/xhr/bindings/open.rs index 2b05c7a455..574575827e 100644 --- a/moli-renderer-v8/src/network_host/xhr/bindings/open.rs +++ b/moli-renderer-v8/src/network_host/xhr/bindings/open.rs @@ -72,6 +72,7 @@ pub(super) fn xhr_open_callback<'s>( set_xhr_state_number(scope, xhr, XHR_OPEN_GENERATION_SLOT, open_generation + 1.0); set_xhr_state_string(scope, xhr, XHR_METHOD_SLOT, &method); set_xhr_state_string(scope, xhr, XHR_URL_SLOT, request_url.as_str()); + set_blob_url_entry(scope, xhr, CapturedBlobUrl::capture(&request_url)); set_xhr_state_string(scope, xhr, XHR_REQUEST_HEADERS_SLOT, "[]"); set_xhr_state_bool(scope, xhr, XHR_ASYNC_SLOT, parsed.async_request); set_xhr_state_number(scope, xhr, XHR_READY_STATE_SLOT, 1.0); diff --git a/moli-renderer-v8/src/network_host/xhr/send.rs b/moli-renderer-v8/src/network_host/xhr/send.rs index 1bf461d5d9..f0ec765a5d 100644 --- a/moli-renderer-v8/src/network_host/xhr/send.rs +++ b/moli-renderer-v8/src/network_host/xhr/send.rs @@ -192,7 +192,11 @@ pub(super) fn xhr_send_callback<'s>( return; } - if let Some(result) = local_url_response_result(&prepared.resolved_url, &prepared.method) { + if let Some(result) = local_url_response_with_blob_entry( + &prepared.resolved_url, + &prepared.method, + prepared.blob_url_entry.as_ref(), + ) { match result { Ok(response) if async_request => { queue_local_xhr_response(scope, host, xhr, prepared, response); diff --git a/moli-renderer-v8/src/network_host/xhr/send/paths.rs b/moli-renderer-v8/src/network_host/xhr/send/paths.rs index 56a0698d3a..12cd106142 100644 --- a/moli-renderer-v8/src/network_host/xhr/send/paths.rs +++ b/moli-renderer-v8/src/network_host/xhr/send/paths.rs @@ -29,6 +29,7 @@ pub(super) fn record_intercepted_xhr( prepared.credentials_mode, prepared.network_partition_key, prepared.policy_context, + prepared.blob_url_entry, PendingSubresourceFetchInfo { internal_id: 0, network_request_handle: None, diff --git a/moli-renderer-v8/src/network_host/xhr/send/request.rs b/moli-renderer-v8/src/network_host/xhr/send/request.rs index 22d73f3ad3..dcd7fb5899 100644 --- a/moli-renderer-v8/src/network_host/xhr/send/request.rs +++ b/moli-renderer-v8/src/network_host/xhr/send/request.rs @@ -21,6 +21,7 @@ pub(super) struct PreparedXhrSendRequest { pub(super) network_partition_key: Option, pub(super) policy_context: crate::types::SubresourcePolicyContext, pub(super) resolved_url: url::Url, + pub(super) blob_url_entry: Option, pub(super) method: String, pub(super) request_headers: Vec<(String, String)>, pub(super) cors_preflight_request_headers: Vec<(String, String)>, @@ -55,7 +56,7 @@ pub(super) fn xhr_dom_debugger_request_url<'s>( pub(super) fn prepare_xhr_send_request<'s>( scope: &mut v8::PinScope<'s, '_>, host: &JsContextHost, - xhr: v8::Local<'_, v8::Object>, + xhr: v8::Local<'s, v8::Object>, method: String, prepared_body: PreparedXhrSendBody, ) -> Result { @@ -99,6 +100,7 @@ pub(super) fn prepare_xhr_send_request<'s>( network_partition_key, policy_context, resolved_url, + blob_url_entry: blob_url_entry(scope, xhr), method, request_headers, cors_preflight_request_headers, 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 7745a8fb37..6e0e14cc53 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 @@ -4355,6 +4355,168 @@ async fn blob_fetch_and_xhr_reject_non_get_methods_in_window_and_worker() { .await; } +#[tokio::test] +async fn blob_url_entries_survive_request_cloning_and_xhr_open_in_window_and_worker() { + run_page_vm_async_test(async move { + for worker in [false, true] { + let mut page_vm = test_page_vm(); + let local_executor = page_vm.local_executor.clone(); + let probe = r#"(async () => { + const check = (value, message) => { if (!value) throw new Error(message); }; + const make = () => URL.createObjectURL(new Blob(['payload'], {type: 'text/plain'})); + const read = async (request) => { + const response = await fetch(request); + check(response.status === 200 && !response.url.includes('#'), 'response metadata'); + check(await response.text() === 'payload', 'captured payload'); + }; + const rejects = async input => { + let failure; + try { await fetch(input); } catch (error) { failure = error; } + check(failure instanceof TypeError, 'fresh revoked URL must reject'); + }; + for (const fragment of ['', '#fragment']) { + const url = make(); + const request = new Request(url + fragment); + const before = request.clone(); + URL.revokeObjectURL(url); + for (const copy of [request, before, request.clone(), new Request(request)]) await read(copy); + await rejects(url + fragment); + await rejects(new Request(url + fragment)); + + for (const inherited of [false, true]) { + for (const member of ['method', 'headers', 'signal']) { + const getterUrl = make(); + const input = inherited ? new Request(getterUrl + fragment) : getterUrl + fragment; + const init = {}; + Object.defineProperty(init, member, {get() { + URL.revokeObjectURL(getterUrl); + return member === 'method' ? 'GET' : member === 'headers' ? [] : null; + }}); + const copy = new Request(input, init); + if (inherited) await read(copy); else await rejects(copy); + } + } + const immediateUrl = make(); + const pending = fetch(immediateUrl + fragment); + URL.revokeObjectURL(immediateUrl); + check(await (await pending).text() === 'payload', 'fetch must capture before returning'); + + for (const async of [false, true]) { + for (const reopen of [false, true]) { + const xhrUrl = make(); + const xhr = new XMLHttpRequest(); + xhr.open('GET', xhrUrl + fragment, async); + URL.revokeObjectURL(xhrUrl); + try { xhr.open('GET', 'http://['); } catch (error) { + check(error.name === 'SyntaxError', 'failed open must preserve previous request'); + } + if (reopen) xhr.open('GET', xhrUrl + fragment, async); + if (async) { + await new Promise(resolve => { xhr.onloadend = resolve; xhr.send(); }); + } else { + let failure; + try { xhr.send(); } catch (error) { failure = error; } + check(reopen ? failure?.name === 'NetworkError' : !failure, 'synchronous outcome'); + } + check(xhr.status === (reopen ? 0 : 200), 'XHR reopened entry status'); + check(xhr.responseText === (reopen ? '' : 'payload'), 'XHR captured body'); + } + } + } + return 'ok'; + })()"#; + let script = if worker { + let source = serde_json::to_string(&format!( + "{probe}.then(postMessage, error => postMessage(String(error)))" + )).unwrap(); + format!(r#"globalThis.__snapshotResult = 'pending'; + const source = URL.createObjectURL(new Blob([{source}])); + const worker = new Worker(source); + worker.onmessage = e => {{ globalThis.__snapshotResult = e.data; worker.terminate(); URL.revokeObjectURL(source); }}; + worker.onerror = e => {{ globalThis.__snapshotResult = e.message; }};"#) + } else { + format!("globalThis.__snapshotResult = 'pending'; {probe}.then(value => {{ globalThis.__snapshotResult = value; }}, error => {{ globalThis.__snapshotResult = String(error); }})") + }; + let result = local_executor.run(async move { + page_vm.vm_mut().eval(&script)?; + drive_websocket_until_done(&mut page_vm, "String(globalThis.__snapshotResult !== 'pending')", "blob entry lifetime checks should finish").await?; + page_vm.vm_mut().eval("globalThis.__snapshotResult") + }).await.expect("blob entry probe should run on owner lane"); + assert_eq!(result, "ok", "worker={worker}"); + } + }).await; +} + +#[tokio::test] +async fn intercepted_blob_url_requests_keep_their_entry_and_respect_url_and_method_overrides() { + run_page_vm_async_test(async move { + for (worker, xhr) in [(false, false), (false, true), (true, false)] { + for change in ["none", "fragment", "url", "method"] { + let mut page_vm = test_page_vm(); + let local_executor = page_vm.local_executor.clone(); + let result = local_executor.run(async move { + page_vm.vm_mut().set_fetch_subresource_interception(true, Some(if xhr { + SubresourceResourceType::Xhr + } else { + SubresourceResourceType::Fetch + })); + let probe = format!(r#"(() => {{ + const original = URL.createObjectURL(new Blob(['payload'])); + const other = URL.createObjectURL(new Blob(['replacement'])); + const finish = value => {{ + URL.revokeObjectURL(other); + {finish} + }}; + if ({xhr}) {{ + const xhr = new XMLHttpRequest(); + xhr.open('GET', original + '#original'); + URL.revokeObjectURL(original); + xhr.onload = () => finish(xhr.responseText); + xhr.onerror = () => finish('error'); + xhr.send(); + }} else {{ + const request = new Request(original + '#original'); + URL.revokeObjectURL(original); + fetch(request).then(r => r.text()).then(finish, () => finish('error')); + }} + {ready} + }})()"#, + finish = if worker { "postMessage({value});" } else { "globalThis.__snapshotResult = value;" }, + ready = if worker { "postMessage({ready: true, other});" } else { "globalThis.__snapshotReady = true; globalThis.__snapshotOther = other;" }, + ); + let script = if worker { + let source = serde_json::to_string(&probe).unwrap(); + format!(r#"globalThis.__snapshotResult = 'pending'; + const source = URL.createObjectURL(new Blob([{source}])); + const worker = new Worker(source); + worker.onmessage = e => {{ + if (e.data.ready) {{ globalThis.__snapshotReady = true; globalThis.__snapshotOther = e.data.other; }} + else {{ globalThis.__snapshotResult = e.data.value; worker.terminate(); URL.revokeObjectURL(source); }} + }};"#) + } else { + format!("globalThis.__snapshotResult = 'pending'; {probe}") + }; + page_vm.vm_mut().eval(&script)?; + drive_websocket_until_done(&mut page_vm, "String(globalThis.__snapshotReady === true)", "blob request should reach interception").await?; + let pending = page_vm.vm_mut().take_pending_subresource_fetch_infos(); + assert_eq!(pending.len(), 1, "worker={worker}, xhr={xhr}, change={change}"); + let pending = &pending[0]; + let url = match change { + "fragment" => { let mut url = pending.url.clone(); url.set_fragment(Some("new")); Some(url) } + "url" => Some(Url::parse(&page_vm.vm_mut().eval("globalThis.__snapshotOther")?)?), + _ => None, + }; + let method = (change == "method").then(|| "POST".to_owned()); + page_vm.continue_pending_subresource_fetch(pending.internal_id, url, method, None, None, false, false)?; + drive_websocket_until_done(&mut page_vm, "String(globalThis.__snapshotResult !== 'pending')", "continued blob request should complete").await?; + page_vm.vm_mut().eval("globalThis.__snapshotResult") + }).await.expect("intercepted blob request should run on owner lane"); + assert_eq!(result, match change { "url" => "replacement", "method" => "error", _ => "payload" }, "worker={worker}, xhr={xhr}, change={change}"); + } + } + }).await; +} + #[tokio::test] async fn window_fetch_revoked_blob_url_records_file_not_found_failure() { 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 4cb0cd3d97..9d26183ca2 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch.rs @@ -1126,6 +1126,7 @@ impl ScriptVm { policy_context, continuation, deferred_request_started, + blob_url_entry, } = pending; let pending = match continuation { PendingSubresourceContinuation::WebSocket(connection) => { @@ -1189,6 +1190,7 @@ impl ScriptVm { policy_context, continuation: target.continuation(), deferred_request_started, + blob_url_entry, }, request_url, request_method, @@ -1244,6 +1246,7 @@ impl ScriptVm { policy_context, continuation: target.continuation(), deferred_request_started, + blob_url_entry, }, request_url, request_method, @@ -1308,6 +1311,7 @@ impl ScriptVm { policy_context, continuation: PendingSubresourceContinuation::CspReport { client_id }, deferred_request_started, + blob_url_entry, }; if !self._context_host.borrow().network_offline() { let maybe_pending = self.continue_csp_report_via_service_worker( @@ -1355,6 +1359,7 @@ impl ScriptVm { policy_context, continuation, deferred_request_started, + blob_url_entry, }, }; let request_url = url.unwrap_or_else(|| pending.info.url.clone()); @@ -1943,6 +1948,7 @@ impl ScriptVm { policy_context, continuation, deferred_request_started, + blob_url_entry, } = pending; let pending = match continuation { PendingSubresourceContinuation::WebSocket(connection) => { @@ -2095,6 +2101,7 @@ impl ScriptVm { policy_context, continuation, deferred_request_started, + blob_url_entry, }, }; let info = pending.info.clone(); @@ -2136,6 +2143,7 @@ impl ScriptVm { policy_context, continuation, deferred_request_started, + blob_url_entry, } = pending; // Request-stage fulfillment has no followed redirects yet. Reuse this // complete head for validation and response materialization. @@ -2460,6 +2468,7 @@ impl ScriptVm { policy_context, continuation, deferred_request_started, + blob_url_entry, }, }; let info = pending.info.clone(); @@ -4009,13 +4018,20 @@ impl ScriptVm { let request_headers = state.request_headers.clone(); let request_body = state.request_body.clone(); let completion_tx = self._context_host.borrow().resource_completion_sender(); + let local_response = crate::network_host::local_url_response_with_blob_entry( + &request.url, + &request.method, + state.pending.blob_url_entry.as_ref(), + ); { let mut host = self._context_host.borrow_mut(); host.begin_active_subresource_request(); host.record_running_subresource_fetch(state); } task_runner.spawn(async move { - let result = + let result = if let Some(result) = local_response { + result.map(crate::protocol_types::NavigationResponse::from) + } else { crate::network_host::fetch_browser_subresource_with_preflight_and_network_metadata( request_client, request, @@ -4028,7 +4044,8 @@ impl ScriptVm { .with_network_request_headers( request_observation.map(|observation| observation.into_headers()), ) - }); + }) + }; let _ = completion_tx.send_async_subresource(AsyncSubresourceFetchCompletion { internal_id, request_url, 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 72b254ce5a..105b650c83 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 @@ -2265,6 +2265,7 @@ async fn streaming_subresource_finish_preserves_response_head_cache_state() { None, ), deferred_request_started: false, + blob_url_entry: None, }, request_url: request_url.clone(), request_method: "GET".to_owned(), @@ -2412,6 +2413,7 @@ async fn async_subresource_failure_network_error_override_preserves_fetch_reject None, ), deferred_request_started: false, + blob_url_entry: None, }, ); Ok(()) @@ -2553,6 +2555,7 @@ async fn streaming_fetch_body_error_records_response_started_then_body_failed() None, ), deferred_request_started: false, + blob_url_entry: None, }, request_url: request_url.clone(), request_method: "GET".to_owned(), @@ -2710,6 +2713,7 @@ fn install_streaming_fetch_response_fixture( Some(cancel_handle), ), deferred_request_started: false, + blob_url_entry: None, }, request_url: request_url.clone(), request_method: "GET".to_owned(), @@ -2818,6 +2822,7 @@ async fn streaming_fetch_body_cancel_aborts_streaming_subresource() { Some(cancel_handle_for_state), ), deferred_request_started: false, + blob_url_entry: None, }, request_url: request_url.clone(), request_method: "GET".to_owned(), @@ -3448,6 +3453,7 @@ async fn streaming_xhr_materialization_failure_errors_body_source_before_close() None, ), deferred_request_started: false, + blob_url_entry: None, }, request_url: request_url.clone(), request_method: "GET".to_owned(), diff --git a/moli-renderer-v8/src/types.rs b/moli-renderer-v8/src/types.rs index 3581a5b1a9..faa6985ead 100644 --- a/moli-renderer-v8/src/types.rs +++ b/moli-renderer-v8/src/types.rs @@ -532,6 +532,7 @@ pub(super) struct PendingSubresourceFetchState { // Window fetches that need CORS preflight emit the actual request-start // after the preflight record, not when the pending fetch is registered. pub(super) deferred_request_started: bool, + pub(super) blob_url_entry: Option, } impl PendingSubresourceFetchState { diff --git a/moli-renderer-v8/src/worker/global_scope/fetch.rs b/moli-renderer-v8/src/worker/global_scope/fetch.rs index 05e67873f6..811263e24b 100644 --- a/moli-renderer-v8/src/worker/global_scope/fetch.rs +++ b/moli-renderer-v8/src/worker/global_scope/fetch.rs @@ -1,4 +1,5 @@ use super::*; +use crate::network_host::{CapturedBlobUrl, local_url_response_with_blob_entry}; use crate::service_worker_runtime::{ ServiceWorkerClientId, ServiceWorkerDirectFetchResult, ServiceWorkerFetchDispatch, ServiceWorkerFetchRequest, ServiceWorkerFetchRequestMetadata, ServiceWorkerRequestDestination, @@ -147,6 +148,7 @@ pub(in crate::worker) fn spawn_worker_fetch_network( referrer_policy: Option, network_partition_key: Option, resolved_url: Url, + blob_url_entry: Option, method: String, body: Option>, headers: Vec<(String, String)>, @@ -158,6 +160,10 @@ pub(in crate::worker) fn spawn_worker_fetch_network( auth: Option, allow_headers_first: bool, ) { + // Resolve local URLs before scheduling: fetch(url) already parsed and + // captured its entry when JavaScript regains control and may revoke it. + let local_response = + local_url_response_with_blob_entry(&resolved_url, &method, blob_url_entry.as_ref()); tokio::task::spawn_local(async move { let loader = load.request_client(); let (result, network_request_headers) = if let Err(message) = @@ -165,7 +171,7 @@ pub(in crate::worker) fn spawn_worker_fetch_network( .validate_request_mode(request_mode, &moli_url::WebOrigin::from_url(&document_url)) { (Err(message), None) - } else if let Some(result) = local_url_response_result(&resolved_url, &method) { + } else if let Some(result) = local_response { ( result .map(|response| WorkerFetchResponse::Materialized(Box::new(response))) @@ -445,6 +451,7 @@ fn spawn_worker_fetch_service_worker( referrer_policy, network_partition_key, resolved_url, + None, method, body, headers, @@ -617,6 +624,7 @@ pub(in crate::worker) fn continue_pending_worker_fetch( network_partition_key, fetch_id, resolved_url, + blob_url_entry, method, body, headers, @@ -658,6 +666,7 @@ pub(in crate::worker) fn continue_pending_worker_fetch( network_partition_key, request.fetch_id, request.url, + pending.blob_url_entry.clone(), request.method, request.body, request.headers, @@ -676,6 +685,7 @@ pub(in crate::worker) fn continue_pending_worker_fetch( state.borrow().referrer_policy.clone(), network_partition_key, resolved_url, + blob_url_entry, method, body.map(|body| body.into_bytes()), headers, @@ -1739,6 +1749,7 @@ pub(in crate::worker) fn worker_fetch_signal_option<'s>( pub(in crate::worker) struct ResolvedWorkerFetchInput<'s> { resolved_url: Url, + blob_url_entry: Option, method: String, body: Option>, body_stream: Option>, @@ -1768,6 +1779,9 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>( let body_stream; let has_stream_body; let inherited = request_input_snapshot(scope, arg0).map_err(|error| error.to_string())?; + let blob_url_entry = inherited + .as_ref() + .and_then(|request| request.blob_url_entry.clone()); let ( url_input, method, @@ -1914,11 +1928,13 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>( metadata.referrer = referrer; } let signal = worker_fetch_signal_option(scope, args, request_like)?; + let blob_url_entry = blob_url_entry.or_else(|| CapturedBlobUrl::capture(&resolved_url)); if consumes_request_body && let Some(request_like) = request_like { crate::network_host::mark_request_input_body_used_for_fetch(scope, request_like); } Ok(ResolvedWorkerFetchInput { resolved_url, + blob_url_entry, method, body, body_stream, @@ -1983,6 +1999,7 @@ pub(in crate::worker) fn worker_fetch_callback<'s>( let ResolvedWorkerFetchInput { resolved_url, + blob_url_entry, method, body, body_stream, @@ -2147,6 +2164,7 @@ pub(in crate::worker) fn worker_fetch_callback<'s>( signal_id, load: load.clone(), request_url: resolved_url.clone(), + blob_url_entry: blob_url_entry.clone(), request_method: method.clone(), request_headers: headers.clone(), request_body: request_body.clone(), @@ -2245,6 +2263,7 @@ pub(in crate::worker) fn worker_fetch_callback<'s>( signal_id, load: load.clone(), request_url: resolved_url.clone(), + blob_url_entry: blob_url_entry.clone(), request_method: method.clone(), request_headers: headers.clone(), request_body, @@ -2300,6 +2319,7 @@ pub(in crate::worker) fn worker_fetch_callback<'s>( referrer_policy, network_partition_key, resolved_url, + blob_url_entry, method, body, headers, diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 77a9b9f8a7..5eb7c83f54 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -83,9 +83,9 @@ use crate::network_host::{ fetch_browser_subresource_raw_stream_with_preflight_headers_and_network_metadata, fetch_browser_subresource_with_preflight_headers_and_network_metadata, filter_cors_exposed_response_headers, filter_headers_for_guard, is_cors_policy_failure_message, - local_url_response_result, parse_fetch_init, prepare_xhr_send_body_from_args, - request_input_snapshot, request_object_credentials_mode, reset_xhr_response_for_request_error, - resolve_context_url, set_xhr_state_bool, set_xhr_state_number, throw_synchronous_xhr_failure, + parse_fetch_init, prepare_xhr_send_body_from_args, request_input_snapshot, + request_object_credentials_mode, reset_xhr_response_for_request_error, resolve_context_url, + set_xhr_state_bool, set_xhr_state_number, throw_synchronous_xhr_failure, validate_fetch_response_headers, validate_fetch_response_security_policy, validate_fetch_response_security_policy_with_body_classified, xhr_author_request_headers, xhr_dispatch_progress_event, xhr_ensure_send_allowed, xhr_state_bool_property, @@ -1094,6 +1094,7 @@ pub(super) struct PendingWorkerFetch { pub(super) signal_id: Option, pub(super) load: ResourceLoadLease, pub(super) request_url: Url, + pub(super) blob_url_entry: Option, pub(super) request_method: String, pub(super) request_headers: Vec<(String, String)>, pub(super) request_body: Option, diff --git a/moli-renderer-v8/src/worker/global_scope/xhr.rs b/moli-renderer-v8/src/worker/global_scope/xhr.rs index 82cfbaa6c1..d077f1b28b 100644 --- a/moli-renderer-v8/src/worker/global_scope/xhr.rs +++ b/moli-renderer-v8/src/worker/global_scope/xhr.rs @@ -1,4 +1,5 @@ use super::*; +use crate::network_host::{CapturedBlobUrl, blob_url_entry, local_url_response_with_blob_entry}; use crossbeam_channel::{after, bounded, never, select}; use moli_webapi_declare::WebApiObject; use std::thread; @@ -7,6 +8,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub(in crate::worker) struct PreparedWorkerXhrSendRequest { document_url: Url, resolved_url: Url, + blob_url_entry: Option, method: String, request_headers: Vec<(String, String)>, send_body: Option>, @@ -408,7 +410,11 @@ pub(crate) fn try_worker_xhr_send_callback<'s>( return true; } - let local_response = local_url_response_result(&prepared.resolved_url, &prepared.method); + let local_response = local_url_response_with_blob_entry( + &prepared.resolved_url, + &prepared.method, + prepared.blob_url_entry.as_ref(), + ); if !async_request && let Some(result) = local_response { match result { Ok(response) => apply_xhr_response(scope, xhr, response), @@ -1166,7 +1172,7 @@ pub(in crate::worker) fn drain_worker_xhr_completion( pub(in crate::worker) fn prepare_worker_xhr_send_request<'s>( scope: &mut v8::PinScope<'s, '_>, state: &Rc>, - xhr: v8::Local<'_, v8::Object>, + xhr: v8::Local<'s, v8::Object>, method: String, prepared_body: PreparedXhrSendBody, ) -> Result { @@ -1190,6 +1196,7 @@ pub(in crate::worker) fn prepare_worker_xhr_send_request<'s>( Ok(PreparedWorkerXhrSendRequest { document_url, resolved_url, + blob_url_entry: blob_url_entry(scope, xhr), method, request_headers, send_body: prepared_body.body,