diff --git a/components/script/dom/response.rs b/components/script/dom/response.rs index 283b7d615aa..cbdfbe94603 100644 --- a/components/script/dom/response.rs +++ b/components/script/dom/response.rs @@ -8,7 +8,7 @@ use std::str::FromStr; use dom_struct::dom_struct; use http::header::HeaderMap as HyperHeaders; use hyper_serde::Serde; -use js::rust::HandleObject; +use js::rust::{HandleObject, HandleValue}; use net_traits::http_status::HttpStatus; use servo_url::ServoUrl; use url::Position; @@ -24,13 +24,13 @@ use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; -use crate::dom::bindings::str::{ByteString, USVString}; +use crate::dom::bindings::str::{ByteString, USVString, serialize_jsval_to_json_utf8}; use crate::dom::globalscope::GlobalScope; use crate::dom::headers::{Guard, Headers, is_obs_text, is_vchar}; use crate::dom::promise::Promise; use crate::dom::readablestream::ReadableStream; use crate::dom::underlyingsourcecontainer::UnderlyingSourceType; -use crate::script_runtime::{CanGc, StreamConsumer}; +use crate::script_runtime::{CanGc, JSContext, StreamConsumer}; #[dom_struct] pub(crate) struct Response { @@ -72,7 +72,7 @@ impl Response { } } - // https://fetch.spec.whatwg.org/#dom-response + /// pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot { Self::new_with_proto(global, None, can_gc) } @@ -142,92 +142,43 @@ fn is_null_body_status(status: u16) -> bool { } impl ResponseMethods for Response { - // https://fetch.spec.whatwg.org/#initialize-a-response + /// fn Constructor( global: &GlobalScope, proto: Option, can_gc: CanGc, - body: Option, + body_init: Option, init: &ResponseBinding::ResponseInit, ) -> Fallible> { - // Step 1 - if init.status < 200 || init.status > 599 { - return Err(Error::Range(format!( - "init's status member should be in the range 200 to 599, inclusive, but is {}", - init.status - ))); - } + // 1. Set this’s response to a new response. + // Our Response/Body types don't actually hold onto an internal fetch Response. + let response = Response::new_with_proto(global, proto, can_gc); - // Step 2 - if !is_valid_status_text(&init.statusText) { - return Err(Error::Type( - "init's statusText member does not match the reason-phrase token production" - .to_string(), - )); - } + // 2. Set this’s headers to a new Headers object with this’s relevant realm, + // whose header list is this’s response’s header list and guard is "response". + response.Headers(can_gc).set_guard(Guard::Response); - let r = Response::new_with_proto(global, proto, can_gc); + // 3. Let bodyWithType be null. + // 4. If body is non-null, then set bodyWithType to the result of extracting body. + let body_with_type = match body_init { + Some(body) => Some(body.extract(global, can_gc)?), + None => None, + }; - // Step 3 & 4 - *r.status.borrow_mut() = HttpStatus::new_raw(init.status, init.statusText.clone().into()); - - // Step 5 - if let Some(ref headers_member) = init.headers { - r.Headers(can_gc).fill(Some(headers_member.clone()))?; - } - - // Step 6 - if let Some(ref body) = body { - // Step 6.1 - if is_null_body_status(init.status) { - return Err(Error::Type( - "Body is non-null but init's status member is a null body status".to_string(), - )); - }; - - // Step 6.2 - let ExtractedBody { - stream, - total_bytes: _, - content_type, - source: _, - } = body.extract(global, can_gc)?; - - r.body_stream.set(Some(&*stream)); - - // Step 6.3 - if let Some(content_type_contents) = content_type { - if !r - .Headers(can_gc) - .Has(ByteString::new(b"Content-Type".to_vec())) - .unwrap() - { - r.Headers(can_gc).Append( - ByteString::new(b"Content-Type".to_vec()), - ByteString::new(content_type_contents.as_bytes().to_vec()), - )?; - } - }; - } else { - // Reset FetchResponse to an in-memory stream with empty byte sequence here for - // no-init-body case - let stream = ReadableStream::new_from_bytes(global, Vec::with_capacity(0), can_gc)?; - r.body_stream.set(Some(&*stream)); - } - - Ok(r) + // 5. Perform *initialize a response* given this, init, and bodyWithType. + initialize_response(global, can_gc, body_with_type, init, response) } - // https://fetch.spec.whatwg.org/#dom-response-error + /// fn Error(global: &GlobalScope, can_gc: CanGc) -> DomRoot { - let r = Response::new(global, can_gc); - *r.response_type.borrow_mut() = DOMResponseType::Error; - r.Headers(can_gc).set_guard(Guard::Immutable); - *r.status.borrow_mut() = HttpStatus::new_error(); - r + let response = Response::new(global, can_gc); + *response.response_type.borrow_mut() = DOMResponseType::Error; + response.Headers(can_gc).set_guard(Guard::Immutable); + *response.status.borrow_mut() = HttpStatus::new_error(); + response } - // https://fetch.spec.whatwg.org/#dom-response-redirect + /// fn Redirect( global: &GlobalScope, url: USVString, @@ -251,31 +202,60 @@ impl ResponseMethods for Response { // Step 4 // see Step 4 continued - let r = Response::new(global, can_gc); + let response = Response::new(global, can_gc); // Step 5 - *r.status.borrow_mut() = HttpStatus::new_raw(status, vec![]); + *response.status.borrow_mut() = HttpStatus::new_raw(status, vec![]); // Step 6 let url_bytestring = ByteString::from_str(url.as_str()).unwrap_or(ByteString::new(b"".to_vec())); - r.Headers(can_gc) + response + .Headers(can_gc) .Set(ByteString::new(b"Location".to_vec()), url_bytestring)?; // Step 4 continued // Headers Guard is set to Immutable here to prevent error in Step 6 - r.Headers(can_gc).set_guard(Guard::Immutable); + response.Headers(can_gc).set_guard(Guard::Immutable); // Step 7 - Ok(r) + Ok(response) } - // https://fetch.spec.whatwg.org/#dom-response-type + /// + #[allow(unsafe_code)] + fn CreateFromJson( + cx: JSContext, + global: &GlobalScope, + data: HandleValue, + init: &ResponseBinding::ResponseInit, + can_gc: CanGc, + ) -> Fallible> { + // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. + let json_str = serialize_jsval_to_json_utf8(cx, data)?; + + // 2. Let body be the result of extracting bytes + // The spec's definition of JSON bytes is a UTF-8 encoding so using a DOMString here handles + // the encoding part. + let body_init = BodyInit::String(json_str); + let mut body = body_init.extract(global, can_gc)?; + + // 3. Let responseObject be the result of creating a Response object, given a new response, + // "response", and the current realm. + let response = Response::new(global, can_gc); + response.Headers(can_gc).set_guard(Guard::Response); + + // 4. Perform initialize a response given responseObject, init, and (body, "application/json"). + body.content_type = Some("application/json".into()); + initialize_response(global, can_gc, Some(body), init, response) + } + + /// fn Type(&self) -> DOMResponseType { *self.response_type.borrow() //into() } - // https://fetch.spec.whatwg.org/#dom-response-url + /// fn Url(&self) -> USVString { USVString(String::from( (*self.url.borrow()) @@ -285,33 +265,33 @@ impl ResponseMethods for Response { )) } - // https://fetch.spec.whatwg.org/#dom-response-redirected + /// fn Redirected(&self) -> bool { return *self.redirected.borrow(); } - // https://fetch.spec.whatwg.org/#dom-response-status + /// fn Status(&self) -> u16 { self.status.borrow().raw_code() } - // https://fetch.spec.whatwg.org/#dom-response-ok + /// fn Ok(&self) -> bool { self.status.borrow().is_success() } - // https://fetch.spec.whatwg.org/#dom-response-statustext + /// fn StatusText(&self) -> ByteString { ByteString::new(self.status.borrow().message().to_vec()) } - // https://fetch.spec.whatwg.org/#dom-response-headers + /// fn Headers(&self, can_gc: CanGc) -> DomRoot { self.headers_reflector .or_init(|| Headers::for_response(&self.global(), can_gc)) } - // https://fetch.spec.whatwg.org/#dom-response-clone + /// fn Clone(&self, can_gc: CanGc) -> Fallible> { // Step 1 if self.is_locked() || self.is_disturbed() { @@ -352,7 +332,7 @@ impl ResponseMethods for Response { Ok(new_response) } - // https://fetch.spec.whatwg.org/#dom-body-bodyused + /// fn BodyUsed(&self) -> bool { self.is_disturbed() } @@ -362,27 +342,27 @@ impl ResponseMethods for Response { self.body() } - // https://fetch.spec.whatwg.org/#dom-body-text + /// fn Text(&self, can_gc: CanGc) -> Rc { consume_body(self, BodyType::Text, can_gc) } - // https://fetch.spec.whatwg.org/#dom-body-blob + /// fn Blob(&self, can_gc: CanGc) -> Rc { consume_body(self, BodyType::Blob, can_gc) } - // https://fetch.spec.whatwg.org/#dom-body-formdata + /// fn FormData(&self, can_gc: CanGc) -> Rc { consume_body(self, BodyType::FormData, can_gc) } - // https://fetch.spec.whatwg.org/#dom-body-json + /// fn Json(&self, can_gc: CanGc) -> Rc { consume_body(self, BodyType::Json, can_gc) } - // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + /// fn ArrayBuffer(&self, can_gc: CanGc) -> Rc { consume_body(self, BodyType::ArrayBuffer, can_gc) } @@ -393,6 +373,80 @@ impl ResponseMethods for Response { } } +/// +fn initialize_response( + global: &GlobalScope, + can_gc: CanGc, + body: Option, + init: &ResponseBinding::ResponseInit, + response: DomRoot, +) -> Result, Error> { + // 1. If init["status"] is not in the range 200 to 599, inclusive, then throw a RangeError. + if init.status < 200 || init.status > 599 { + return Err(Error::Range(format!( + "init's status member should be in the range 200 to 599, inclusive, but is {}", + init.status + ))); + } + + // 2. If init["statusText"] is not the empty string and does not match the reason-phrase token production, + // then throw a TypeError. + if !is_valid_status_text(&init.statusText) { + return Err(Error::Type( + "init's statusText member does not match the reason-phrase token production" + .to_string(), + )); + } + + // 3. Set response’s response’s status to init["status"]. + // 4. Set response’s response’s status message to init["statusText"]. + *response.status.borrow_mut() = + HttpStatus::new_raw(init.status, init.statusText.clone().into()); + + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. + if let Some(ref headers_member) = init.headers { + response + .Headers(can_gc) + .fill(Some(headers_member.clone()))?; + } + + // 6. If body is non-null, then: + if let Some(ref body) = body { + // 6.1 If response’s status is a null body status, then throw a TypeError. + if is_null_body_status(init.status) { + return Err(Error::Type( + "Body is non-null but init's status member is a null body status".to_string(), + )); + }; + + // 6.2 Set response’s body to body’s body. + response.body_stream.set(Some(&*body.stream)); + + // 6.3 If body’s type is non-null and response’s header list does not contain `Content-Type`, + // then append (`Content-Type`, body’s type) to response’s header list. + if let Some(content_type_contents) = &body.content_type { + if !response + .Headers(can_gc) + .Has(ByteString::new(b"Content-Type".to_vec())) + .unwrap() + { + response.Headers(can_gc).Append( + ByteString::new(b"Content-Type".to_vec()), + ByteString::new(content_type_contents.as_bytes().to_vec()), + )?; + } + }; + } else { + // Reset FetchResponse to an in-memory stream with empty byte sequence here for + // no-init-body case. This is because the Response/Body types here do not hold onto a + // fetch Response object. + let stream = ReadableStream::new_from_bytes(global, Vec::with_capacity(0), can_gc)?; + response.body_stream.set(Some(&*stream)); + } + + Ok(response) +} + fn serialize_without_fragment(url: &ServoUrl) -> &str { &url[..Position::AfterQuery] } diff --git a/components/script_bindings/codegen/Bindings.conf b/components/script_bindings/codegen/Bindings.conf index 9684d9a4c5d..c457bf70b85 100644 --- a/components/script_bindings/codegen/Bindings.conf +++ b/components/script_bindings/codegen/Bindings.conf @@ -551,7 +551,7 @@ DOMInterfaces = { }, 'Response': { - 'canGc': ['Error', 'Redirect', 'Clone', 'Text', 'Blob', 'FormData', 'Json', 'ArrayBuffer', 'Headers', 'Bytes'], + 'canGc': ['Error', 'Redirect', 'Clone', 'CreateFromJson', 'Text', 'Blob', 'FormData', 'Json', 'ArrayBuffer', 'Headers', 'Bytes'], }, 'RTCPeerConnection': { diff --git a/components/script_bindings/str.rs b/components/script_bindings/str.rs index 09d48512f3e..0ef6e0c528a 100644 --- a/components/script_bindings/str.rs +++ b/components/script_bindings/str.rs @@ -10,14 +10,19 @@ use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::str::FromStr; use std::sync::LazyLock; -use std::{fmt, ops, str}; +use std::{fmt, ops, slice, str}; use cssparser::CowRcStr; use html5ever::{LocalName, Namespace}; +use js::rust::wrappers::ToJSON; +use js::rust::{HandleObject, HandleValue}; use num_traits::Zero; use regex::Regex; use stylo_atoms::Atom; +use crate::error::Error; +use crate::script_runtime::JSContext as SafeJSContext; + /// Encapsulates the IDL `ByteString` type. #[derive(Clone, Debug, Default, Eq, JSTraceable, MallocSizeOf, PartialEq)] pub struct ByteString(Vec); @@ -293,6 +298,64 @@ impl DOMString { } } +/// Because this converts to a DOMString it becomes UTF-8 encoded which is closer to +/// the spec definition of +/// but we generally do not operate on anything that is truly a WTF-16 string. +/// +/// +pub fn serialize_jsval_to_json_utf8( + cx: SafeJSContext, + data: HandleValue, +) -> Result { + #[repr(C)] + struct ToJSONCallbackData { + string: Option, + } + + let mut out_str = ToJSONCallbackData { string: None }; + + #[allow(unsafe_code)] + unsafe extern "C" fn write_callback( + string: *const u16, + len: u32, + data: *mut std::ffi::c_void, + ) -> bool { + let data = data as *mut ToJSONCallbackData; + let string_chars = slice::from_raw_parts(string, len as usize); + (*data) + .string + .get_or_insert_with(Default::default) + .push_str(&String::from_utf16_lossy(string_chars)); + true + } + + // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). + unsafe { + let stringify_result = ToJSON( + *cx, + data, + HandleObject::null(), + HandleValue::null(), + Some(write_callback), + &mut out_str as *mut ToJSONCallbackData as *mut _, + ); + // Note: ToJSON returns false when a JS error is thrown, so we need to return + // JSFailed to propagate the raised exception + if !stringify_result { + return Err(Error::JSFailed); + } + } + + // 2. If result is undefined, then throw a TypeError. + // Note: ToJSON will not call the callback if the data cannot be serialized. + // 3. Assert: result is a string. + // 4. Return result. + out_str + .string + .map(Into::into) + .ok_or_else(|| Error::Type("unable to serialize JSON".to_owned())) +} + impl Borrow for DOMString { #[inline] fn borrow(&self) -> &str { diff --git a/components/script_bindings/webidls/Response.webidl b/components/script_bindings/webidls/Response.webidl index 0ced0c13794..d37538d4b6b 100644 --- a/components/script_bindings/webidls/Response.webidl +++ b/components/script_bindings/webidls/Response.webidl @@ -9,6 +9,7 @@ interface Response { [Throws] constructor(optional BodyInit? body = null, optional ResponseInit init = {}); [NewObject] static Response error(); [NewObject, Throws] static Response redirect(USVString url, optional unsigned short status = 302); + [NewObject, Throws, BinaryName="createFromJson"] static Response json(any data, optional ResponseInit init = {}); readonly attribute ResponseType type; diff --git a/tests/wpt/meta/fetch/api/idlharness.any.js.ini b/tests/wpt/meta/fetch/api/idlharness.any.js.ini index 44d299531ad..58df7ba3188 100644 --- a/tests/wpt/meta/fetch/api/idlharness.any.js.ini +++ b/tests/wpt/meta/fetch/api/idlharness.any.js.ini @@ -29,12 +29,6 @@ [Request interface: new Request('about:blank') must inherit property "duplex" with the proper type] expected: FAIL - [Response interface: operation json(any, optional ResponseInit)] - expected: FAIL - - [Response interface: calling json(any, optional ResponseInit) on new Response() with too few arguments must throw TypeError] - expected: FAIL - [idlharness.any.sharedworker.html] expected: ERROR @@ -70,12 +64,6 @@ [Request interface: new Request('about:blank') must inherit property "duplex" with the proper type] expected: FAIL - [Response interface: operation json(any, optional ResponseInit)] - expected: FAIL - - [Response interface: calling json(any, optional ResponseInit) on new Response() with too few arguments must throw TypeError] - expected: FAIL - [idlharness.any.serviceworker.html] expected: ERROR diff --git a/tests/wpt/meta/fetch/api/response/response-static-json.any.js.ini b/tests/wpt/meta/fetch/api/response/response-static-json.any.js.ini index 580482226f0..830bfcd339c 100644 --- a/tests/wpt/meta/fetch/api/response/response-static-json.any.js.ini +++ b/tests/wpt/meta/fetch/api/response/response-static-json.any.js.ini @@ -1,73 +1,3 @@ -[response-static-json.any.worker.html] - [Check response returned by static json() with init undefined] - expected: FAIL - - [Check response returned by static json() with init {"status":400}] - expected: FAIL - - [Check response returned by static json() with init {"statusText":"foo"}] - expected: FAIL - - [Check response returned by static json() with init {"headers":{}}] - expected: FAIL - - [Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}}] - expected: FAIL - - [Check response returned by static json() with init {"headers":{"x-foo":"bar"}}] - expected: FAIL - - [Check static json() encodes JSON objects correctly] - expected: FAIL - - [Check static json() propagates JSON serializer errors] - expected: FAIL - - [Check response returned by static json() with input 𝌆] - expected: FAIL - - [Check response returned by static json() with input U+df06U+d834] - expected: FAIL - - [Check response returned by static json() with input U+dead] - expected: FAIL - - -[response-static-json.any.html] - [Check response returned by static json() with init undefined] - expected: FAIL - - [Check response returned by static json() with init {"status":400}] - expected: FAIL - - [Check response returned by static json() with init {"statusText":"foo"}] - expected: FAIL - - [Check response returned by static json() with init {"headers":{}}] - expected: FAIL - - [Check response returned by static json() with init {"headers":{"content-type":"foo/bar"}}] - expected: FAIL - - [Check response returned by static json() with init {"headers":{"x-foo":"bar"}}] - expected: FAIL - - [Check static json() encodes JSON objects correctly] - expected: FAIL - - [Check static json() propagates JSON serializer errors] - expected: FAIL - - [Check response returned by static json() with input 𝌆] - expected: FAIL - - [Check response returned by static json() with input U+df06U+d834] - expected: FAIL - - [Check response returned by static json() with input U+dead] - expected: FAIL - - [response-static-json.any.sharedworker.html] expected: ERROR