mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
refactor(fetch): preserve typed argument conversion errors
Carry WebIdlError and structured fetch validation errors through Window and Worker argument conversion, formatting them only at the rejection boundary. Propagate Proxy.has exceptions before further conversion or body consumption, and cover exception identity, validation messages, realms, and side effects. Fix existing XHR response-document header type mismatches in the parser and its regression fixture so the workspace builds with byte-valued headers. Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo nextest run --no-fail-fast (18490 passed, 14 skipped)
This commit is contained in:
@@ -123,9 +123,9 @@ pub(crate) use self::media::{
|
||||
start_media_element_resource_fetch,
|
||||
};
|
||||
pub(in crate::network_host) use self::preflight_events::CorsPreflightNetworkObserver;
|
||||
pub(crate) use self::request::convert_fetch_arguments;
|
||||
pub(in crate::network_host) use self::request::normalize_request_method;
|
||||
pub(crate) use self::request::request_constructor_callback;
|
||||
pub(crate) use self::request::{FetchArgumentError, convert_fetch_arguments};
|
||||
pub(crate) use self::request::{
|
||||
mark_request_input_body_used_for_fetch, request_input_snapshot,
|
||||
try_resolve_request_constructor_url, try_resolve_request_constructor_url_for_base,
|
||||
|
||||
@@ -353,23 +353,25 @@ fn window_fetch_callback_in_relevant_realm<'s>(
|
||||
fn window_fetch_signal_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
) -> Result<Option<v8::Local<'s, v8::Value>>, String> {
|
||||
) -> Result<Option<v8::Local<'s, v8::Value>>, crate::webidl::WebIdlError> {
|
||||
let signal_key = v8str(scope, "signal");
|
||||
if args.length() > 1 {
|
||||
let init_arg = args.get(1);
|
||||
if !init_arg.is_null_or_undefined()
|
||||
&& let Ok(init) = v8::Local::<v8::Object>::try_from(init_arg)
|
||||
&& init
|
||||
.has(scope, signal_key.into())
|
||||
.ok_or("Failed to read RequestInit.signal")?
|
||||
&& init.has(scope, signal_key.into()).ok_or_else(|| {
|
||||
crate::webidl::WebIdlError::pending_exception(crate::webidl::Context::member(
|
||||
"RequestInit",
|
||||
"signal",
|
||||
))
|
||||
})?
|
||||
{
|
||||
return crate::webidl::property_result(
|
||||
scope,
|
||||
init,
|
||||
"signal",
|
||||
crate::webidl::Context::member("RequestInit", "signal"),
|
||||
)
|
||||
.map_err(|error| error.to_string());
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,8 +385,7 @@ fn window_fetch_signal_value<'s>(
|
||||
request_like,
|
||||
"signal",
|
||||
crate::webidl::Context::member("Request", "signal"),
|
||||
)
|
||||
.map_err(|error| error.to_string());
|
||||
);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -394,19 +395,19 @@ fn validate_window_fetch_signal<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
host: &mut JsContextHost,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Result<Option<v8::Local<'s, v8::Object>>, String> {
|
||||
) -> Result<Option<v8::Local<'s, v8::Object>>, crate::webidl::WebIdlError> {
|
||||
if value.is_null_or_undefined() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Ok(signal) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
return Err(
|
||||
"Failed to execute 'fetch' on 'Window': signal must be an AbortSignal.".to_owned(),
|
||||
);
|
||||
return Err(crate::webidl::WebIdlError::custom_message(
|
||||
"Failed to execute 'fetch' on 'Window': signal must be an AbortSignal.",
|
||||
));
|
||||
};
|
||||
if !host.is_abort_signal(scope, signal) {
|
||||
return Err(
|
||||
"Failed to execute 'fetch' on 'Window': signal must be an AbortSignal.".to_owned(),
|
||||
);
|
||||
return Err(crate::webidl::WebIdlError::custom_message(
|
||||
"Failed to execute 'fetch' on 'Window': signal must be an AbortSignal.",
|
||||
));
|
||||
}
|
||||
Ok(Some(signal))
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ pub(super) fn prepare_window_fetch_request<'s>(
|
||||
let cors_preflight_request_headers = request_headers.clone();
|
||||
let request_headers =
|
||||
merge_byte_string_request_headers(host.extra_http_headers(), &request_headers);
|
||||
let resolved_url = resolve_context_url(&base_url, &parsed.url, None)?;
|
||||
let resolved_url =
|
||||
resolve_context_url(&base_url, &parsed.url, None).map_err(|error| error.to_string())?;
|
||||
|
||||
Ok(PreparedWindowFetchRequest {
|
||||
frame_id,
|
||||
|
||||
@@ -22,15 +22,14 @@ pub(super) struct ParsedWindowFetchInput {
|
||||
pub(super) fn parse_window_fetch_input<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
) -> Result<ParsedWindowFetchInput, String> {
|
||||
) -> Result<ParsedWindowFetchInput, FetchArgumentError> {
|
||||
if args.length() < 1 {
|
||||
return Err(
|
||||
webidl::WebIdlError::missing_required(webidl::Context::argument("fetch", 1))
|
||||
.to_string(),
|
||||
webidl::WebIdlError::missing_required(webidl::Context::argument("fetch", 1)).into(),
|
||||
);
|
||||
}
|
||||
let arg0 = args.get(0);
|
||||
let inherited = request_input_snapshot(scope, arg0).map_err(|error| error.to_string())?;
|
||||
let inherited = request_input_snapshot(scope, arg0)?;
|
||||
if let Some(inherited) = inherited {
|
||||
let req_obj = v8::Local::<v8::Object>::try_from(arg0).expect("request-like object");
|
||||
let url = inherited.url.clone();
|
||||
@@ -130,13 +129,14 @@ pub(super) fn parse_window_fetch_input<'s>(
|
||||
fn validate_no_cors_method(
|
||||
request_mode: moli_fetch::RequestMode,
|
||||
method: &str,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<(), FetchArgumentError> {
|
||||
if request_mode == moli_fetch::RequestMode::NoCors
|
||||
&& !moli_fetch::is_cors_safelisted_method(method)
|
||||
{
|
||||
return Err(format!(
|
||||
"Failed to execute 'fetch': method `{method}` is unsupported in no-cors mode."
|
||||
));
|
||||
return Err(FetchArgumentError::UnsupportedNoCorsMethod {
|
||||
method: method.to_owned(),
|
||||
interface: None,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -144,8 +144,7 @@ fn validate_no_cors_method(
|
||||
fn fetch_request_info_url<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<String, webidl::WebIdlError> {
|
||||
webidl::convert::<webidl::UsvString>(scope, value, webidl::Context::argument("fetch", 1))
|
||||
.map(Into::into)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod bindings;
|
||||
mod error;
|
||||
mod init;
|
||||
mod input;
|
||||
|
||||
@@ -8,6 +9,7 @@ use super::headers::{
|
||||
use super::*;
|
||||
|
||||
pub(crate) use self::bindings::request_constructor_callback;
|
||||
pub(crate) use self::error::FetchArgumentError;
|
||||
pub(crate) use self::init::convert_fetch_arguments;
|
||||
pub(in crate::network_host) use self::init::request_credentials_mode_label;
|
||||
pub(crate) use self::init::{parse_fetch_init, request_object_credentials_mode};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use crate::network_host::url_helpers::ResolveContextUrlError;
|
||||
use crate::webidl;
|
||||
use std::fmt;
|
||||
|
||||
/// Argument conversion failures remain structured until the fetch binding
|
||||
/// turns them into a JavaScript rejection.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum FetchArgumentError {
|
||||
WebIdl(webidl::WebIdlError),
|
||||
UnsupportedNoCorsMethod {
|
||||
method: String,
|
||||
interface: Option<&'static str>,
|
||||
},
|
||||
Url(ResolveContextUrlError),
|
||||
}
|
||||
|
||||
impl FetchArgumentError {
|
||||
pub(super) fn throw(&self, scope: &mut v8::PinScope<'_, '_>) {
|
||||
match self {
|
||||
Self::WebIdl(error) => webidl::throw_error(scope, error),
|
||||
_ => webidl::throw_type_error(scope, &self.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<webidl::WebIdlError> for FetchArgumentError {
|
||||
fn from(error: webidl::WebIdlError) -> Self {
|
||||
Self::WebIdl(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResolveContextUrlError> for FetchArgumentError {
|
||||
fn from(error: ResolveContextUrlError) -> Self {
|
||||
Self::Url(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FetchArgumentError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::WebIdl(error) => error.fmt(f),
|
||||
Self::UnsupportedNoCorsMethod { method, interface } => {
|
||||
write!(f, "Failed to execute 'fetch'")?;
|
||||
if let Some(interface) = interface {
|
||||
write!(f, " on '{interface}'")?;
|
||||
}
|
||||
write!(f, ": method `{method}` is unsupported in no-cors mode.")
|
||||
}
|
||||
Self::Url(error) => error.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FetchArgumentError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::WebIdl(error) => Some(error),
|
||||
Self::Url(error) => Some(error),
|
||||
Self::UnsupportedNoCorsMethod { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,18 @@ use std::str::FromStr;
|
||||
/// is consumed or any request is dispatched.
|
||||
pub(crate) fn convert_fetch_arguments<'s, T>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
convert: impl FnOnce(&mut v8::PinScope<'s, '_>) -> Result<T, String>,
|
||||
convert: impl FnOnce(&mut v8::PinScope<'s, '_>) -> Result<T, FetchArgumentError>,
|
||||
) -> Result<T, v8::Local<'s, v8::Value>> {
|
||||
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
|
||||
let mut conversion_scope = try_catch.init();
|
||||
let result = convert(&mut conversion_scope);
|
||||
if !conversion_scope.has_caught()
|
||||
&& let Err(error) = &result
|
||||
{
|
||||
// WebIDL's PendingException already carries an exception through V8;
|
||||
// only ordinary validation failures need a new TypeError here.
|
||||
error.throw(&mut conversion_scope);
|
||||
}
|
||||
if conversion_scope.has_caught() {
|
||||
let exception = conversion_scope
|
||||
.exception()
|
||||
@@ -21,11 +28,7 @@ pub(crate) fn convert_fetch_arguments<'s, T>(
|
||||
conversion_scope.reset();
|
||||
return Err(exception);
|
||||
}
|
||||
result.map_err(|message| {
|
||||
crate::util::v8_string(&conversion_scope, &message)
|
||||
.map(|message| v8::Exception::type_error(&conversion_scope, message))
|
||||
.unwrap_or_else(|| v8::undefined(&conversion_scope).into())
|
||||
})
|
||||
result.map_err(|_| v8::undefined(&conversion_scope).into())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -135,7 +138,7 @@ pub(crate) fn parse_fetch_init<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
index: i32,
|
||||
) -> Result<ParsedFetchInit, String> {
|
||||
) -> Result<ParsedFetchInit, webidl::WebIdlError> {
|
||||
if args.length() <= index {
|
||||
return Ok(ParsedFetchInit::default());
|
||||
}
|
||||
@@ -144,31 +147,32 @@ pub(crate) fn parse_fetch_init<'s>(
|
||||
return Ok(ParsedFetchInit::default());
|
||||
};
|
||||
|
||||
let init = webidl::parse_dictionary_object::<RequestInitMembers>(scope, init_object)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let init = webidl::parse_dictionary_object::<RequestInitMembers>(scope, init_object)?;
|
||||
let priority = init.priority.map(|value| value.0);
|
||||
let method_present = init_object
|
||||
.has(scope, v8str(scope, "method").into())
|
||||
.unwrap_or(false);
|
||||
.ok_or_else(|| {
|
||||
webidl::WebIdlError::pending_exception(webidl::Context::member("RequestInit", "method"))
|
||||
})?;
|
||||
let method = init
|
||||
.method
|
||||
.map(|s| normalize_request_method(&s).map_err(str::to_owned))
|
||||
.map(|s| normalize_request_method(&s).map_err(webidl::WebIdlError::custom_message))
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| "GET".to_owned());
|
||||
|
||||
let body_present = init_object
|
||||
.has(scope, v8str(scope, "body").into())
|
||||
.unwrap_or(false);
|
||||
.ok_or_else(|| {
|
||||
webidl::WebIdlError::pending_exception(webidl::Context::member("RequestInit", "body"))
|
||||
})?;
|
||||
let prepared_body = webidl::property_result(
|
||||
scope,
|
||||
init_object,
|
||||
"body",
|
||||
webidl::Context::member("RequestInit", "body"),
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
)?
|
||||
.map(|value| body_init(scope, value, webidl::Context::member("RequestInit", "body")))
|
||||
.transpose()
|
||||
.map_err(|error| error.to_string())?
|
||||
.transpose()?
|
||||
.flatten();
|
||||
let body = prepared_body.as_ref().map(|body| body.bytes.clone());
|
||||
let body_content_type = prepared_body
|
||||
@@ -177,12 +181,19 @@ pub(crate) fn parse_fetch_init<'s>(
|
||||
if body.as_ref().is_some_and(|body| !body.is_empty())
|
||||
&& matches!(method.as_str(), "GET" | "HEAD")
|
||||
{
|
||||
return Err("Request with GET/HEAD method cannot have body".to_owned());
|
||||
return Err(webidl::WebIdlError::custom_message(
|
||||
"Request with GET/HEAD method cannot have body",
|
||||
));
|
||||
}
|
||||
|
||||
let headers_present = init_object
|
||||
.has(scope, v8str(scope, "headers").into())
|
||||
.unwrap_or(false);
|
||||
.ok_or_else(|| {
|
||||
webidl::WebIdlError::pending_exception(webidl::Context::member(
|
||||
"RequestInit",
|
||||
"headers",
|
||||
))
|
||||
})?;
|
||||
let mut extra_headers = init.headers.unwrap_or_default();
|
||||
append_default_body_content_type(&mut extra_headers, body_content_type.as_deref());
|
||||
|
||||
@@ -216,14 +227,13 @@ pub(crate) fn parse_fetch_init<'s>(
|
||||
pub(crate) fn request_object_credentials_mode<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Result<Option<moli_fetch::RequestCredentialsMode>, String> {
|
||||
) -> Result<Option<moli_fetch::RequestCredentialsMode>, webidl::WebIdlError> {
|
||||
if is_branded_request_object(scope, object) {
|
||||
return Ok(request_slot_string(scope, object, REQUEST_CREDENTIALS_SLOT)
|
||||
.and_then(|value| RequestCredentialsMode::from_str(&value).ok()));
|
||||
}
|
||||
webidl::parse_dictionary_object::<RequestInitMembers>(scope, object)
|
||||
.map(|parsed| parsed.credentials_mode.map(|value| value.0))
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(in crate::network_host) fn request_credentials_mode_label(
|
||||
|
||||
@@ -350,7 +350,9 @@ pub(crate) fn try_resolve_request_constructor_url_for_scope(
|
||||
return Ok(input.to_owned());
|
||||
}
|
||||
if let Some(base_url) = base_url {
|
||||
return resolve_context_url(&base_url, input, None).map(|url| url.to_string());
|
||||
return resolve_context_url(&base_url, input, None)
|
||||
.map(|url| url.to_string())
|
||||
.map_err(|error| error.to_string());
|
||||
}
|
||||
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
@@ -363,12 +365,16 @@ pub(crate) fn try_resolve_request_constructor_url_for_scope(
|
||||
let (_, document_url, _) = effective_subresource_request_scope(scope, host, None);
|
||||
document_url
|
||||
});
|
||||
resolve_context_url(&document_url, input, None).map(|url| url.to_string())
|
||||
resolve_context_url(&document_url, input, None)
|
||||
.map(|url| url.to_string())
|
||||
.map_err(|error| error.to_string())
|
||||
} else if let Some(worker_url) = crate::context_bootstrap::current_worker_script_url(scope) {
|
||||
if worker_url.scheme() == "blob" {
|
||||
return Err(format!("Failed to parse URL from {input}"));
|
||||
}
|
||||
resolve_context_url(&worker_url, input, None).map(|url| url.to_string())
|
||||
resolve_context_url(&worker_url, input, None)
|
||||
.map(|url| url.to_string())
|
||||
.map_err(|error| error.to_string())
|
||||
} else {
|
||||
Ok(input.to_owned())
|
||||
}
|
||||
|
||||
@@ -1,18 +1,59 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ResolveContextUrlError {
|
||||
Base {
|
||||
input: String,
|
||||
source: url::ParseError,
|
||||
},
|
||||
Url {
|
||||
input: String,
|
||||
source: url::ParseError,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for ResolveContextUrlError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Base { input, source } => {
|
||||
write!(f, "failed to resolve base url `{input}`: {source}")
|
||||
}
|
||||
Self::Url { input, source } => {
|
||||
write!(f, "failed to resolve url `{input}`: {source}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ResolveContextUrlError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Base { source, .. } | Self::Url { source, .. } => Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_context_url(
|
||||
document_url: &url::Url,
|
||||
input: &str,
|
||||
base: Option<&str>,
|
||||
) -> std::result::Result<url::Url, String> {
|
||||
) -> Result<url::Url, ResolveContextUrlError> {
|
||||
let resolved_base = match base {
|
||||
Some(base) => url::Url::parse(base)
|
||||
.or_else(|_| document_url.join(base))
|
||||
.map_err(|error| format!("failed to resolve base url `{base}`: {error}"))?,
|
||||
.map_err(|source| ResolveContextUrlError::Base {
|
||||
input: base.to_owned(),
|
||||
source,
|
||||
})?,
|
||||
None => document_url.clone(),
|
||||
};
|
||||
|
||||
url::Url::parse(input)
|
||||
.or_else(|_| resolved_base.join(input))
|
||||
.map_err(|error| format!("failed to resolve url `{input}`: {error}"))
|
||||
.map_err(|source| ResolveContextUrlError::Url {
|
||||
input: input.to_owned(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::network_host) fn merge_subresource_request_headers(
|
||||
|
||||
@@ -78,8 +78,8 @@ pub(super) fn prepare_xhr_send_request<'s>(
|
||||
} = environment;
|
||||
let policy_context = effective_subresource_policy_context(scope, host, owner);
|
||||
let network_partition_key = active_subresource_network_partition_key(host, owner);
|
||||
let resolved_url =
|
||||
resolve_context_url(&base_url, &url_str, None).map_err(XhrSendPrepareError::Url)?;
|
||||
let resolved_url = resolve_context_url(&base_url, &url_str, None)
|
||||
.map_err(|error| XhrSendPrepareError::Url(error.to_string()))?;
|
||||
let (request_headers, cors_preflight_request_headers) =
|
||||
xhr_request_headers(scope, host, xhr, prepared_body.default_content_type);
|
||||
let credentials_mode =
|
||||
|
||||
@@ -7074,7 +7074,7 @@ async fn blob_url_revocation_uses_window_and_worker_creator_storage_keys() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming_input() {
|
||||
async fn request_and_fetch_argument_errors_preserve_exceptions_without_side_effects() {
|
||||
run_page_vm_async_test(async move {
|
||||
for worker in [false, true] {
|
||||
let mut page_vm = test_page_vm();
|
||||
@@ -7083,7 +7083,7 @@ async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming
|
||||
const check = (value, message) => { if (!value) throw new Error(message); };
|
||||
const url = 'data:text/plain,must-not-fetch';
|
||||
for (const api of ['Request', 'fetch']) {
|
||||
for (const member of ['method', 'headers', 'signal']) {
|
||||
for (const member of ['method', 'headers', 'signal', 'body']) {
|
||||
for (const sentinel of [undefined, null, false, 0, 'sentinel', {}, new Error('sentinel')]) {
|
||||
const input = new Request(url, {method:'POST', body:'kept'});
|
||||
const init = {};
|
||||
@@ -7103,6 +7103,52 @@ async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming
|
||||
}
|
||||
}
|
||||
}
|
||||
const rejection = async promise => {
|
||||
check(promise instanceof Promise, 'fetch conversion must return a Promise');
|
||||
let rejected = false, caught;
|
||||
await promise.then(() => {}, error => { rejected = true; caught = error; });
|
||||
check(rejected, 'invalid arguments must reject');
|
||||
return caught;
|
||||
};
|
||||
for (const sentinel of [undefined, null, false, 0, 'sentinel', {}, new Error('sentinel')]) {
|
||||
const throwSentinel = () => { throw sentinel; };
|
||||
const inits = [
|
||||
{method: {toString: throwSentinel}},
|
||||
{headers: {[Symbol.iterator]: throwSentinel}},
|
||||
...['method', 'body', 'headers', 'signal'].map(member => new Proxy({}, {
|
||||
has(target, key) {
|
||||
if (key === member) throw sentinel;
|
||||
return Reflect.has(target, key);
|
||||
},
|
||||
})),
|
||||
];
|
||||
for (const init of inits) {
|
||||
const input = new Request(url, {method: 'POST', body: 'kept'});
|
||||
check(Object.is(await rejection(fetch(input, init)), sentinel), 'nested conversion exception identity');
|
||||
check(!input.bodyUsed, 'nested conversion failure consumed input body');
|
||||
}
|
||||
check(Object.is(await rejection(fetch({toString: throwSentinel})), sentinel), 'URL conversion exception identity');
|
||||
}
|
||||
const interfaceName = typeof document === 'undefined' ? 'DedicatedWorkerGlobalScope' : 'Window';
|
||||
const noCorsInterface = typeof document === 'undefined' ? " on 'DedicatedWorkerGlobalScope'" : '';
|
||||
for (const [init, message] of [
|
||||
[{method: 'TRACE'}, 'Request method is forbidden'],
|
||||
[{method: 'GET', body: 'invalid'}, 'Request with GET/HEAD method cannot have body'],
|
||||
[{mode: 'invalid'}, 'RequestInit: mode is not a valid enum value of type RequestMode'],
|
||||
[{signal: false}, `Failed to execute 'fetch' on '${interfaceName}': signal must be an AbortSignal.`],
|
||||
[{signal: {}}, `Failed to execute 'fetch' on '${interfaceName}': signal must be an AbortSignal.`],
|
||||
[{mode: 'no-cors', method: 'PATCH'}, `Failed to execute 'fetch'${noCorsInterface}: method \`PATCH\` is unsupported in no-cors mode.`],
|
||||
]) {
|
||||
const input = new Request(url, {method: 'POST', body: 'kept'});
|
||||
const error = await rejection(fetch(input, init));
|
||||
check(error instanceof TypeError, 'validation must reject with a TypeError');
|
||||
check(error.message === message, 'validation message: ' + error.message);
|
||||
check(!input.bodyUsed, 'validation failure consumed input body');
|
||||
}
|
||||
const missing = await rejection(fetch());
|
||||
check(missing instanceof TypeError && missing.message === 'fetch: Argument 1 is required', 'missing argument validation');
|
||||
const badUrl = await rejection(fetch('http://['));
|
||||
check(badUrl instanceof TypeError && badUrl.message.startsWith('failed to resolve url `http://[`:'), 'invalid URL validation');
|
||||
if (typeof document !== 'undefined') {
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame);
|
||||
@@ -7113,6 +7159,10 @@ async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming
|
||||
let caught;
|
||||
await promise.catch(error => { caught = error; });
|
||||
check(caught === sentinel, 'cross-realm exception identity');
|
||||
const invalidSignal = other.fetch.call(window, url, {signal: false});
|
||||
check(invalidSignal instanceof other.Promise, 'validation rejection must belong to function realm');
|
||||
await invalidSignal.catch(error => { caught = error; });
|
||||
check(caught instanceof other.TypeError, 'validation TypeError must belong to function realm');
|
||||
frame.remove();
|
||||
}
|
||||
return 'ok';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::network_host::convert_fetch_arguments;
|
||||
use crate::network_host::{FetchArgumentError, convert_fetch_arguments};
|
||||
use crate::service_worker_runtime::{
|
||||
ServiceWorkerClientId, ServiceWorkerDirectFetchResult, ServiceWorkerFetchDispatch,
|
||||
ServiceWorkerFetchRequest, ServiceWorkerFetchRequestMetadata, ServiceWorkerRequestDestination,
|
||||
@@ -1771,21 +1771,19 @@ pub(in crate::worker) fn make_rejected_promise_with_value<'s>(
|
||||
pub(in crate::worker) fn validate_worker_fetch_signal<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Result<Option<v8::Local<'s, v8::Object>>, String> {
|
||||
) -> Result<Option<v8::Local<'s, v8::Object>>, webidl::WebIdlError> {
|
||||
if value.is_null_or_undefined() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Ok(signal) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
return Err(
|
||||
"Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': signal must be an AbortSignal."
|
||||
.to_owned(),
|
||||
);
|
||||
return Err(webidl::WebIdlError::custom_message(
|
||||
"Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': signal must be an AbortSignal.",
|
||||
));
|
||||
};
|
||||
if worker_abort_signal_id(scope, signal).is_none() {
|
||||
return Err(
|
||||
"Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': signal must be an AbortSignal."
|
||||
.to_owned(),
|
||||
);
|
||||
return Err(webidl::WebIdlError::custom_message(
|
||||
"Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': signal must be an AbortSignal.",
|
||||
));
|
||||
}
|
||||
Ok(Some(signal))
|
||||
}
|
||||
@@ -1794,23 +1792,25 @@ pub(in crate::worker) fn worker_fetch_signal_option<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
request_like: Option<v8::Local<'s, v8::Object>>,
|
||||
) -> Result<Option<v8::Local<'s, v8::Object>>, String> {
|
||||
) -> Result<Option<v8::Local<'s, v8::Object>>, webidl::WebIdlError> {
|
||||
let signal_key = v8str(scope, "signal");
|
||||
if args.length() > 1 {
|
||||
let init_arg = args.get(1);
|
||||
if !init_arg.is_null_or_undefined()
|
||||
&& let Ok(init) = v8::Local::<v8::Object>::try_from(init_arg)
|
||||
&& init
|
||||
.has(scope, signal_key.into())
|
||||
.ok_or("Failed to read RequestInit.signal")?
|
||||
&& init.has(scope, signal_key.into()).ok_or_else(|| {
|
||||
webidl::WebIdlError::pending_exception(webidl::Context::member(
|
||||
"RequestInit",
|
||||
"signal",
|
||||
))
|
||||
})?
|
||||
{
|
||||
let signal = webidl::property_result(
|
||||
scope,
|
||||
init,
|
||||
"signal",
|
||||
webidl::Context::member("RequestInit", "signal"),
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
)?
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
return validate_worker_fetch_signal(scope, signal);
|
||||
}
|
||||
@@ -1821,8 +1821,7 @@ pub(in crate::worker) fn worker_fetch_signal_option<'s>(
|
||||
request_like,
|
||||
"signal",
|
||||
webidl::Context::member("Request", "signal"),
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
)?
|
||||
{
|
||||
return validate_worker_fetch_signal(scope, signal);
|
||||
}
|
||||
@@ -1846,17 +1845,16 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
base_url: &Url,
|
||||
) -> Result<ResolvedWorkerFetchInput<'s>, String> {
|
||||
) -> Result<ResolvedWorkerFetchInput<'s>, FetchArgumentError> {
|
||||
if args.length() < 1 {
|
||||
return Err(
|
||||
webidl::WebIdlError::missing_required(webidl::Context::argument("fetch", 1))
|
||||
.to_string(),
|
||||
webidl::WebIdlError::missing_required(webidl::Context::argument("fetch", 1)).into(),
|
||||
);
|
||||
}
|
||||
let arg0 = args.get(0);
|
||||
let mut request_like = None;
|
||||
let mut consumes_request_body = false;
|
||||
let inherited = request_input_snapshot(scope, arg0).map_err(|error| error.to_string())?;
|
||||
let inherited = request_input_snapshot(scope, arg0)?;
|
||||
let (
|
||||
url_input,
|
||||
method,
|
||||
@@ -1938,8 +1936,7 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
|
||||
arg0,
|
||||
webidl::Context::argument("fetch", 1),
|
||||
)
|
||||
.map(String::from)
|
||||
.map_err(|error| error.to_string())?;
|
||||
.map(String::from)?;
|
||||
let init = parse_fetch_init(scope, args, 1)?;
|
||||
let request_mode = init.request_mode.unwrap_or(moli_fetch::RequestMode::Cors);
|
||||
validate_worker_no_cors_method(request_mode, &init.method)?;
|
||||
@@ -1993,11 +1990,12 @@ pub(in crate::worker) fn resolve_worker_fetch_input<'s>(
|
||||
pub(in crate::worker) fn validate_worker_no_cors_method(
|
||||
request_mode: RequestMode,
|
||||
method: &str,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<(), FetchArgumentError> {
|
||||
if request_mode == RequestMode::NoCors && !moli_fetch::is_cors_safelisted_method(method) {
|
||||
return Err(format!(
|
||||
"Failed to execute 'fetch' on 'DedicatedWorkerGlobalScope': method `{method}` is unsupported in no-cors mode."
|
||||
));
|
||||
return Err(FetchArgumentError::UnsupportedNoCorsMethod {
|
||||
method: method.to_owned(),
|
||||
interface: Some("DedicatedWorkerGlobalScope"),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1182,7 +1182,7 @@ pub(in crate::worker) fn prepare_worker_xhr_send_request<'s>(
|
||||
)
|
||||
})?;
|
||||
let resolved_url = resolve_context_url(&document_url, &url_str, None)
|
||||
.map_err(WorkerXhrSendPrepareError::Request)?;
|
||||
.map_err(|error| WorkerXhrSendPrepareError::Request(error.to_string()))?;
|
||||
let request_headers =
|
||||
xhr_author_request_headers(scope, xhr, prepared_body.default_content_type);
|
||||
let credentials_mode =
|
||||
|
||||
Reference in New Issue
Block a user