Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/runtime/src/fetch/headers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! The `Headers` JavaScript class, implemented as [`JsHeaders`].
//! The `Headers` JavaScript class, implemented as [`JsHeaders`].
//!
//! See <https://developer.mozilla.org/en-US/docs/Web/API/Headers>.
#![allow(clippy::needless_pass_by_value)]
Expand Down
4 changes: 2 additions & 2 deletions core/runtime/src/fetch/headers_iterator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! This module implements the `HeadersIterator` object.
//! This module implements the `HeadersIterator` object.
//!
//! More information:
//! - [Fetch specification][spec]
Expand Down Expand Up @@ -42,7 +42,7 @@ pub(crate) struct HeadersIterator {

#[boa_class(rename = "Headers Iterator")]
impl HeadersIterator {
/// Prevent direct construction `HeadersIterator` instances are only
/// Prevent direct construction ΓÇö `HeadersIterator` instances are only
/// created internally via [`HeadersIterator::create_headers_iterator`].
#[boa(constructor)]
fn constructor() -> JsResult<Self> {
Expand Down
206 changes: 205 additions & 1 deletion core/runtime/src/fetch/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,204 @@ use boa_engine::{
use either::Either;
use std::mem;

/// The [mode][mdn] for a `Request`.
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
#[derive(Debug, Clone, Trace, Finalize)]
pub enum RequestMode {
/// Navigation request.
Navigate,
/// Same-origin request.
SameOrigin,
/// No CORS check.
NoCors,
/// CORS-enabled request.
Cors,
}

impl TryFromJs for RequestMode {
fn try_from_js(value: &JsValue, context: &mut boa_engine::Context) -> JsResult<Self> {
let s = value.to_string(context)?;
match s.to_std_string_escaped().as_str() {
"navigate" => Ok(Self::Navigate),
"same-origin" => Ok(Self::SameOrigin),
"no-cors" => Ok(Self::NoCors),
"cors" => Ok(Self::Cors),
other => Err(js_error!(
TypeError: "Request constructor: mode '{}' is not a supported value",
other
)),
}
}
}

/// The [credentials][mdn] mode for a `Request`.
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
#[derive(Debug, Clone, Trace, Finalize)]
pub enum RequestCredentials {
/// Never send or receive cookies.
Omit,
/// Send credentials only if the URL is on the same origin.
SameOrigin,
/// Always send credentials.
Include,
}

impl TryFromJs for RequestCredentials {
fn try_from_js(value: &JsValue, context: &mut boa_engine::Context) -> JsResult<Self> {
let s = value.to_string(context)?;
match s.to_std_string_escaped().as_str() {
"omit" => Ok(Self::Omit),
"same-origin" => Ok(Self::SameOrigin),
"include" => Ok(Self::Include),
other => Err(js_error!(
TypeError: "Request constructor: credentials '{}' is not a supported value",
other
)),
}
}
}

/// The [cache][mdn] mode for a `Request`.
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/cache
#[derive(Debug, Clone, Trace, Finalize)]
pub enum RequestCache {
/// The browser looks for a matching request in its HTTP cache.
Default,
/// The browser fetches the resource from the remote server without first
/// looking in the cache, and will not update the cache with the response.
NoStore,
/// The browser fetches the resource from the remote server without first
/// looking in the cache, but then will update the cache with the response.
Reload,
/// The browser looks for a matching request in its HTTP cache; if found,
/// the browser revalidates the response.
NoCache,
/// The browser looks for a matching request in its HTTP cache; if found,
/// returns the cached response even if stale.
ForceCache,
/// The browser looks for a matching request in its HTTP cache; if found,
/// returns it. Otherwise returns a `504 Gateway Timeout`.
OnlyIfCached,
}

impl TryFromJs for RequestCache {
fn try_from_js(value: &JsValue, context: &mut boa_engine::Context) -> JsResult<Self> {
let s = value.to_string(context)?;
match s.to_std_string_escaped().as_str() {
"default" => Ok(Self::Default),
"no-store" => Ok(Self::NoStore),
"reload" => Ok(Self::Reload),
"no-cache" => Ok(Self::NoCache),
"force-cache" => Ok(Self::ForceCache),
"only-if-cached" => Ok(Self::OnlyIfCached),
other => Err(js_error!(
TypeError: "Request constructor: cache '{}' is not a supported value",
other
)),
}
}
}

/// The [redirect][mdn] mode for a `Request`.
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/redirect
#[derive(Debug, Clone, Trace, Finalize)]
pub enum RequestRedirect {
/// Automatically follow redirects.
Follow,
/// Abort with an error if a redirect occurs.
Error,
/// Return a filtered response whose type is `opaqueredirect`.
Manual,
}

impl TryFromJs for RequestRedirect {
fn try_from_js(value: &JsValue, context: &mut boa_engine::Context) -> JsResult<Self> {
let s = value.to_string(context)?;
match s.to_std_string_escaped().as_str() {
"follow" => Ok(Self::Follow),
"error" => Ok(Self::Error),
"manual" => Ok(Self::Manual),
other => Err(js_error!(
TypeError: "Request constructor: redirect '{}' is not a supported value",
other
)),
}
}
}

/// The [referrer policy][mdn] for a `Request`.
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request/referrerPolicy
#[derive(Debug, Clone, Trace, Finalize)]
pub enum ReferrerPolicy {
/// No policy (empty string).
Empty,
/// No referrer information sent.
NoReferrer,
/// Send the referrer for same-protocol destinations (HTTPS→HTTPS).
NoReferrerWhenDowngrade,
/// Only send referrer for same-origin requests.
SameOrigin,
/// Only send the origin as the referrer.
Origin,
/// Send origin for cross-origin; full URL for same-origin.
StrictOrigin,
/// Send origin for cross-origin; full URL for same-origin.
OriginWhenCrossOrigin,
/// Same as origin-when-cross-origin but only for same-protocol.
StrictOriginWhenCrossOrigin,
/// Always send the full URL as the referrer.
UnsafeUrl,
}

impl TryFromJs for ReferrerPolicy {
fn try_from_js(value: &JsValue, context: &mut boa_engine::Context) -> JsResult<Self> {
let s = value.to_string(context)?;
match s.to_std_string_escaped().as_str() {
"" => Ok(Self::Empty),
"no-referrer" => Ok(Self::NoReferrer),
"no-referrer-when-downgrade" => Ok(Self::NoReferrerWhenDowngrade),
"same-origin" => Ok(Self::SameOrigin),
"origin" => Ok(Self::Origin),
"strict-origin" => Ok(Self::StrictOrigin),
"origin-when-cross-origin" => Ok(Self::OriginWhenCrossOrigin),
"strict-origin-when-cross-origin" => Ok(Self::StrictOriginWhenCrossOrigin),
"unsafe-url" => Ok(Self::UnsafeUrl),
other => Err(js_error!(
TypeError: "Request constructor: referrerPolicy '{}' is not a supported value",
other
)),
}
}
}

/// A [RequestInit][mdn] object. This is a JavaScript object (not a
/// class) that can be used as options for creating a [`JsRequest`].
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
// TODO: This class does not contain all fields that are defined in the spec.
// NOTE: This does not yet contain *all* fields from the spec, but it
// now supports several of the most common ones.
#[derive(Debug, Clone, TryFromJs, Trace, Finalize)]
pub struct RequestInit {
body: Option<JsValue>,
headers: Option<JsHeaders>,
method: Option<Convert<JsString>>,

// Additional RequestInit fields from the Fetch spec. These are
// parsed and validated via their enum TryFromJs implementations,
// but most are not yet wired through to the underlying HTTP client.
mode: Option<RequestMode>,
credentials: Option<RequestCredentials>,
cache: Option<RequestCache>,
redirect: Option<RequestRedirect>,
referrer: Option<Convert<JsString>>,
referrer_policy: Option<ReferrerPolicy>,
integrity: Option<Convert<JsString>>,
keepalive: Option<bool>,
signal: Option<JsObject>,
}

Expand Down Expand Up @@ -85,6 +273,22 @@ impl RequestInit {
builder = builder.method(method.as_str());
}

// The enum fields (mode, credentials, cache, redirect,
// referrer_policy) are already validated by their TryFromJs
// implementations. They are accepted here but treated as no-ops
// until the runtime wires them to the underlying HTTP client.
drop(self.mode.take());
drop(self.credentials.take());
drop(self.cache.take());
drop(self.redirect.take());
drop(self.referrer_policy.take());

// `referrer`, `integrity` and `keepalive` are accepted for now but
// treated as no-ops until the runtime makes use of them.
drop(self.referrer.take());
drop(self.integrity.take());
let _ = self.keepalive.take();

if let Some(body) = &self.body {
// TODO: add more support types.
if let Some(body) = body.as_string() {
Expand Down
43 changes: 43 additions & 0 deletions core/runtime/src/fetch/tests/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,46 @@ fn request_clone_signal_override() {
}),
]);
}

#[test]
fn request_init_accepts_valid_mode_and_credentials() {
run_test_actions([
TestAction::inspect_context(|ctx| {
let fetcher = TestFetcher::default();
crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch");
}),
TestAction::run(
r#"
// Valid mode / credentials combinations should not throw.
new Request("http://unit.test", { mode: "cors", credentials: "include" });
"#,
),
]);
}

#[test]
fn request_init_rejects_invalid_mode() {
run_test_actions([
TestAction::inspect_context(|ctx| {
let fetcher = TestFetcher::default();
crate::fetch::register(fetcher, None, ctx).expect("failed to register fetch");
}),
TestAction::run(
r#"
let threw = false;
try {
new Request("http://unit.test", { mode: "totally-invalid-mode" });
} catch (e) {
if (e instanceof TypeError) {
threw = true;
} else {
throw new Error("Expected TypeError, got " + e);
}
}
if (!threw) {
throw new Error("Expected TypeError to be thrown");
}
"#,
),
]);
}
Loading