mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
fix(fetch): retain parsed blob URL entries across revocation
Capture shared blob bytes when Request is constructed or XHR is opened, and retain the entry through cloning, Window/Worker fetches, and request interception. Fresh URLs and reopened XHRs still observe revocation. Private isolate-scoped carriers release their references on GC or isolate teardown, while requests survive cleanup of the creating realm. Validation: cargo fmt, workspace Clippy with warnings denied, and nextest (17986 passed, 13 skipped). Fixes seven upstream WPT failures; 58 cases show no regressions. Additional contract checks improve from 332 to 440 out of 448; eight pre-existing getter-exception failures remain separate.
This commit is contained in:
@@ -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<u8>, 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::<u64, u64>::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::<u64, u64>::default();
|
||||
|
||||
@@ -411,6 +411,10 @@ pub(super) fn object_url_bytes_and_type(url: &str) -> Option<(Vec<u8>, 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>,
|
||||
|
||||
@@ -558,6 +558,7 @@ impl JsContextHost {
|
||||
request_origin: moli_url::WebOrigin,
|
||||
network_partition_key: Option<String>,
|
||||
policy_context: crate::types::SubresourcePolicyContext,
|
||||
blob_url_entry: Option<crate::network_host::CapturedBlobUrl>,
|
||||
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<String>,
|
||||
policy_context: crate::types::SubresourcePolicyContext,
|
||||
blob_url_entry: Option<crate::network_host::CapturedBlobUrl>,
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Self> {
|
||||
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<super::Response> {
|
||||
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<RefCell<Entries>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Entries {
|
||||
next_id: u64,
|
||||
values: HashMap<u64, (v8::Weak<v8::Object>, CapturedBlobUrl)>,
|
||||
}
|
||||
|
||||
pub(crate) fn set_blob_url_entry<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
entry: Option<CapturedBlobUrl>,
|
||||
) {
|
||||
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>() {
|
||||
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<CapturedBlobUrl> {
|
||||
let carrier = get_private_value(scope, owner, BLOB_URL_ENTRY_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())?;
|
||||
let id = get_private_value(scope, carrier, ID_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::BigInt>::try_from(value).ok())?
|
||||
.u64_value()
|
||||
.0;
|
||||
scope
|
||||
.get_slot::<Store>()?
|
||||
.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::<Store>()
|
||||
.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());
|
||||
}
|
||||
}
|
||||
@@ -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<Response> {
|
||||
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<u8>, 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<Response> {
|
||||
negotiated_http_version: None,
|
||||
},
|
||||
body_bytes,
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn data_url_response(url: &url::Url) -> Option<Response> {
|
||||
@@ -63,13 +67,25 @@ pub(crate) fn local_url_response(url: &url::Url) -> Option<Response> {
|
||||
pub(crate) fn local_url_response_result(
|
||||
url: &url::Url,
|
||||
method: &str,
|
||||
) -> Option<Result<Response, String>> {
|
||||
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<Result<Response, String>> {
|
||||
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")))
|
||||
|
||||
@@ -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<Option<Response>, 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| {
|
||||
|
||||
@@ -13,6 +13,7 @@ pub(super) struct PreparedWindowFetchRequest {
|
||||
pub(super) document_referrer_policy: Option<String>,
|
||||
pub(super) policy_context: crate::types::SubresourcePolicyContext,
|
||||
pub(super) resolved_url: url::Url,
|
||||
pub(super) blob_url_entry: Option<CapturedBlobUrl>,
|
||||
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,
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::str::FromStr;
|
||||
|
||||
pub(super) struct ParsedWindowFetchInput {
|
||||
pub(super) url: String,
|
||||
pub(super) blob_url_entry: Option<CapturedBlobUrl>,
|
||||
pub(super) method: String,
|
||||
pub(super) body: Option<Vec<u8>>,
|
||||
pub(super) body_stream: Option<v8::Global<v8::Object>>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ use moli_fetch::RequestRedirectMode;
|
||||
|
||||
pub(super) struct RequestConstructionState {
|
||||
pub(super) url_resolved: String,
|
||||
pub(super) blob_url_entry: Option<CapturedBlobUrl>,
|
||||
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,
|
||||
|
||||
@@ -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<CapturedBlobUrl>,
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -21,6 +21,7 @@ pub(super) struct PreparedXhrSendRequest {
|
||||
pub(super) network_partition_key: Option<String>,
|
||||
pub(super) policy_context: crate::types::SubresourcePolicyContext,
|
||||
pub(super) resolved_url: url::Url,
|
||||
pub(super) blob_url_entry: Option<CapturedBlobUrl>,
|
||||
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<PreparedXhrSendRequest, XhrSendPrepareError> {
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<crate::network_host::CapturedBlobUrl>,
|
||||
}
|
||||
|
||||
impl PendingSubresourceFetchState {
|
||||
|
||||
@@ -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<String>,
|
||||
network_partition_key: Option<String>,
|
||||
resolved_url: Url,
|
||||
blob_url_entry: Option<CapturedBlobUrl>,
|
||||
method: String,
|
||||
body: Option<Vec<u8>>,
|
||||
headers: Vec<(String, String)>,
|
||||
@@ -158,6 +160,10 @@ pub(in crate::worker) fn spawn_worker_fetch_network(
|
||||
auth: Option<crate::protocol_types::SubresourceAuthCredentials>,
|
||||
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<CapturedBlobUrl>,
|
||||
method: String,
|
||||
body: Option<Vec<u8>>,
|
||||
body_stream: Option<v8::Global<v8::Object>>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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<u32>,
|
||||
pub(super) load: ResourceLoadLease,
|
||||
pub(super) request_url: Url,
|
||||
pub(super) blob_url_entry: Option<crate::network_host::CapturedBlobUrl>,
|
||||
pub(super) request_method: String,
|
||||
pub(super) request_headers: Vec<(String, String)>,
|
||||
pub(super) request_body: Option<String>,
|
||||
|
||||
@@ -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<CapturedBlobUrl>,
|
||||
method: String,
|
||||
request_headers: Vec<(String, String)>,
|
||||
send_body: Option<Vec<u8>>,
|
||||
@@ -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<RefCell<WorkerGlobalState>>,
|
||||
xhr: v8::Local<'_, v8::Object>,
|
||||
xhr: v8::Local<'s, v8::Object>,
|
||||
method: String,
|
||||
prepared_body: PreparedXhrSendBody,
|
||||
) -> Result<PreparedWorkerXhrSendRequest, WorkerXhrSendPrepareError> {
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user