mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Implements https://fetch.spec.whatwg.org/#dom-response-json Restructured the constructor to follow the spec more closely with a separate "initialize the response" algorithm. Testing: There are existing WPT tests for this. --------- Signed-off-by: Sebastian C <sebsebmc@gmail.com>
530 lines
19 KiB
Rust
530 lines
19 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||
|
||
use std::rc::Rc;
|
||
use std::str::FromStr;
|
||
|
||
use dom_struct::dom_struct;
|
||
use http::header::HeaderMap as HyperHeaders;
|
||
use hyper_serde::Serde;
|
||
use js::rust::{HandleObject, HandleValue};
|
||
use net_traits::http_status::HttpStatus;
|
||
use servo_url::ServoUrl;
|
||
use url::Position;
|
||
|
||
use crate::body::{BodyMixin, BodyType, Extractable, ExtractedBody, consume_body};
|
||
use crate::dom::bindings::cell::DomRefCell;
|
||
use crate::dom::bindings::codegen::Bindings::HeadersBinding::HeadersMethods;
|
||
use crate::dom::bindings::codegen::Bindings::ResponseBinding;
|
||
use crate::dom::bindings::codegen::Bindings::ResponseBinding::{
|
||
ResponseMethods, ResponseType as DOMResponseType,
|
||
};
|
||
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, 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, JSContext, StreamConsumer};
|
||
|
||
#[dom_struct]
|
||
pub(crate) struct Response {
|
||
reflector_: Reflector,
|
||
headers_reflector: MutNullableDom<Headers>,
|
||
#[no_trace]
|
||
status: DomRefCell<HttpStatus>,
|
||
response_type: DomRefCell<DOMResponseType>,
|
||
#[no_trace]
|
||
url: DomRefCell<Option<ServoUrl>>,
|
||
#[no_trace]
|
||
url_list: DomRefCell<Vec<ServoUrl>>,
|
||
/// The stream of <https://fetch.spec.whatwg.org/#body>.
|
||
body_stream: MutNullableDom<ReadableStream>,
|
||
#[ignore_malloc_size_of = "StreamConsumer"]
|
||
stream_consumer: DomRefCell<Option<StreamConsumer>>,
|
||
redirected: DomRefCell<bool>,
|
||
}
|
||
|
||
#[allow(non_snake_case)]
|
||
impl Response {
|
||
pub(crate) fn new_inherited(global: &GlobalScope, can_gc: CanGc) -> Response {
|
||
let stream = ReadableStream::new_with_external_underlying_source(
|
||
global,
|
||
UnderlyingSourceType::FetchResponse,
|
||
can_gc,
|
||
)
|
||
.expect("Failed to create ReadableStream with external underlying source");
|
||
Response {
|
||
reflector_: Reflector::new(),
|
||
headers_reflector: Default::default(),
|
||
status: DomRefCell::new(HttpStatus::default()),
|
||
response_type: DomRefCell::new(DOMResponseType::Default),
|
||
url: DomRefCell::new(None),
|
||
url_list: DomRefCell::new(vec![]),
|
||
body_stream: MutNullableDom::new(Some(&*stream)),
|
||
stream_consumer: DomRefCell::new(None),
|
||
redirected: DomRefCell::new(false),
|
||
}
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-response>
|
||
pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Response> {
|
||
Self::new_with_proto(global, None, can_gc)
|
||
}
|
||
|
||
fn new_with_proto(
|
||
global: &GlobalScope,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<Response> {
|
||
reflect_dom_object_with_proto(
|
||
Box::new(Response::new_inherited(global, can_gc)),
|
||
global,
|
||
proto,
|
||
can_gc,
|
||
)
|
||
}
|
||
|
||
pub(crate) fn error_stream(&self, error: Error, can_gc: CanGc) {
|
||
if let Some(body) = self.body_stream.get() {
|
||
body.error_native(error, can_gc);
|
||
}
|
||
}
|
||
}
|
||
|
||
impl BodyMixin for Response {
|
||
fn is_disturbed(&self) -> bool {
|
||
self.body_stream
|
||
.get()
|
||
.is_some_and(|stream| stream.is_disturbed())
|
||
}
|
||
|
||
fn is_locked(&self) -> bool {
|
||
self.body_stream
|
||
.get()
|
||
.is_some_and(|stream| stream.is_locked())
|
||
}
|
||
|
||
fn body(&self) -> Option<DomRoot<ReadableStream>> {
|
||
self.body_stream.get()
|
||
}
|
||
|
||
fn get_mime_type(&self, can_gc: CanGc) -> Vec<u8> {
|
||
let headers = self.Headers(can_gc);
|
||
headers.extract_mime_type()
|
||
}
|
||
}
|
||
|
||
// https://fetch.spec.whatwg.org/#redirect-status
|
||
fn is_redirect_status(status: u16) -> bool {
|
||
status == 301 || status == 302 || status == 303 || status == 307 || status == 308
|
||
}
|
||
|
||
// https://tools.ietf.org/html/rfc7230#section-3.1.2
|
||
fn is_valid_status_text(status_text: &ByteString) -> bool {
|
||
// reason-phrase = *( HTAB / SP / VCHAR / obs-text )
|
||
for byte in status_text.iter() {
|
||
if !(*byte == b'\t' || *byte == b' ' || is_vchar(*byte) || is_obs_text(*byte)) {
|
||
return false;
|
||
}
|
||
}
|
||
true
|
||
}
|
||
|
||
// https://fetch.spec.whatwg.org/#null-body-status
|
||
fn is_null_body_status(status: u16) -> bool {
|
||
status == 101 || status == 204 || status == 205 || status == 304
|
||
}
|
||
|
||
impl ResponseMethods<crate::DomTypeHolder> for Response {
|
||
/// <https://fetch.spec.whatwg.org/#dom-response>
|
||
fn Constructor(
|
||
global: &GlobalScope,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
body_init: Option<BodyInit>,
|
||
init: &ResponseBinding::ResponseInit,
|
||
) -> Fallible<DomRoot<Response>> {
|
||
// 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);
|
||
|
||
// 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);
|
||
|
||
// 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,
|
||
};
|
||
|
||
// 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<Response> {
|
||
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,
|
||
status: u16,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<Response>> {
|
||
// Step 1
|
||
let base_url = global.api_base_url();
|
||
let parsed_url = base_url.join(&url.0);
|
||
|
||
// Step 2
|
||
let url = match parsed_url {
|
||
Ok(url) => url,
|
||
Err(_) => return Err(Error::Type("ServoUrl could not be parsed".to_string())),
|
||
};
|
||
|
||
// Step 3
|
||
if !is_redirect_status(status) {
|
||
return Err(Error::Range("status is not a redirect status".to_string()));
|
||
}
|
||
|
||
// Step 4
|
||
// see Step 4 continued
|
||
let response = Response::new(global, can_gc);
|
||
|
||
// Step 5
|
||
*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()));
|
||
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
|
||
response.Headers(can_gc).set_guard(Guard::Immutable);
|
||
|
||
// Step 7
|
||
Ok(response)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-response-json>
|
||
#[allow(unsafe_code)]
|
||
fn CreateFromJson(
|
||
cx: JSContext,
|
||
global: &GlobalScope,
|
||
data: HandleValue,
|
||
init: &ResponseBinding::ResponseInit,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<Response>> {
|
||
// 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)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-response-type>
|
||
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())
|
||
.as_ref()
|
||
.map(serialize_without_fragment)
|
||
.unwrap_or(""),
|
||
))
|
||
}
|
||
|
||
/// <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<Headers> {
|
||
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<DomRoot<Response>> {
|
||
// Step 1
|
||
if self.is_locked() || self.is_disturbed() {
|
||
return Err(Error::Type("cannot clone a disturbed response".to_string()));
|
||
}
|
||
|
||
// Step 2
|
||
let new_response = Response::new(&self.global(), can_gc);
|
||
new_response
|
||
.Headers(can_gc)
|
||
.copy_from_headers(self.Headers(can_gc))?;
|
||
new_response
|
||
.Headers(can_gc)
|
||
.set_guard(self.Headers(can_gc).get_guard());
|
||
|
||
// https://fetch.spec.whatwg.org/#concept-response-clone
|
||
// Instead of storing a net_traits::Response internally, we
|
||
// only store the relevant fields, and only clone them here
|
||
*new_response.response_type.borrow_mut() = *self.response_type.borrow();
|
||
new_response
|
||
.status
|
||
.borrow_mut()
|
||
.clone_from(&self.status.borrow());
|
||
new_response.url.borrow_mut().clone_from(&self.url.borrow());
|
||
new_response
|
||
.url_list
|
||
.borrow_mut()
|
||
.clone_from(&self.url_list.borrow());
|
||
|
||
if let Some(stream) = self.body_stream.get().clone() {
|
||
new_response.body_stream.set(Some(&*stream));
|
||
}
|
||
|
||
// Step 3
|
||
// TODO: This step relies on promises, which are still unimplemented.
|
||
|
||
// Step 4
|
||
Ok(new_response)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-body-bodyused>
|
||
fn BodyUsed(&self) -> bool {
|
||
self.is_disturbed()
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-body-body>
|
||
fn GetBody(&self) -> Option<DomRoot<ReadableStream>> {
|
||
self.body()
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-body-text>
|
||
fn Text(&self, can_gc: CanGc) -> Rc<Promise> {
|
||
consume_body(self, BodyType::Text, can_gc)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-body-blob>
|
||
fn Blob(&self, can_gc: CanGc) -> Rc<Promise> {
|
||
consume_body(self, BodyType::Blob, can_gc)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-body-formdata>
|
||
fn FormData(&self, can_gc: CanGc) -> Rc<Promise> {
|
||
consume_body(self, BodyType::FormData, can_gc)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-body-json>
|
||
fn Json(&self, can_gc: CanGc) -> Rc<Promise> {
|
||
consume_body(self, BodyType::Json, can_gc)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-body-arraybuffer>
|
||
fn ArrayBuffer(&self, can_gc: CanGc) -> Rc<Promise> {
|
||
consume_body(self, BodyType::ArrayBuffer, can_gc)
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#dom-body-bytes>
|
||
fn Bytes(&self, can_gc: CanGc) -> std::rc::Rc<Promise> {
|
||
consume_body(self, BodyType::Bytes, can_gc)
|
||
}
|
||
}
|
||
|
||
/// <https://fetch.spec.whatwg.org/#initialize-a-response>
|
||
fn initialize_response(
|
||
global: &GlobalScope,
|
||
can_gc: CanGc,
|
||
body: Option<ExtractedBody>,
|
||
init: &ResponseBinding::ResponseInit,
|
||
response: DomRoot<Response>,
|
||
) -> Result<DomRoot<Response>, 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]
|
||
}
|
||
|
||
impl Response {
|
||
pub(crate) fn set_type(&self, new_response_type: DOMResponseType, can_gc: CanGc) {
|
||
*self.response_type.borrow_mut() = new_response_type;
|
||
self.set_response_members_by_type(new_response_type, can_gc);
|
||
}
|
||
|
||
pub(crate) fn set_headers(
|
||
&self,
|
||
option_hyper_headers: Option<Serde<HyperHeaders>>,
|
||
can_gc: CanGc,
|
||
) {
|
||
self.Headers(can_gc)
|
||
.set_headers(match option_hyper_headers {
|
||
Some(hyper_headers) => hyper_headers.into_inner(),
|
||
None => HyperHeaders::new(),
|
||
});
|
||
}
|
||
|
||
pub(crate) fn set_status(&self, status: &HttpStatus) {
|
||
self.status.borrow_mut().clone_from(status);
|
||
}
|
||
|
||
pub(crate) fn set_final_url(&self, final_url: ServoUrl) {
|
||
*self.url.borrow_mut() = Some(final_url);
|
||
}
|
||
|
||
pub(crate) fn set_redirected(&self, is_redirected: bool) {
|
||
*self.redirected.borrow_mut() = is_redirected;
|
||
}
|
||
|
||
fn set_response_members_by_type(&self, response_type: DOMResponseType, can_gc: CanGc) {
|
||
match response_type {
|
||
DOMResponseType::Error => {
|
||
*self.status.borrow_mut() = HttpStatus::new_error();
|
||
self.set_headers(None, can_gc);
|
||
},
|
||
DOMResponseType::Opaque => {
|
||
*self.url_list.borrow_mut() = vec![];
|
||
*self.status.borrow_mut() = HttpStatus::new_error();
|
||
self.set_headers(None, can_gc);
|
||
self.body_stream.set(None);
|
||
},
|
||
DOMResponseType::Opaqueredirect => {
|
||
*self.status.borrow_mut() = HttpStatus::new_error();
|
||
self.set_headers(None, can_gc);
|
||
self.body_stream.set(None);
|
||
},
|
||
DOMResponseType::Default => {},
|
||
DOMResponseType::Basic => {},
|
||
DOMResponseType::Cors => {},
|
||
}
|
||
}
|
||
|
||
pub(crate) fn set_stream_consumer(&self, sc: Option<StreamConsumer>) {
|
||
*self.stream_consumer.borrow_mut() = sc;
|
||
}
|
||
|
||
pub(crate) fn stream_chunk(&self, chunk: Vec<u8>, can_gc: CanGc) {
|
||
// Note, are these two actually mutually exclusive?
|
||
if let Some(stream_consumer) = self.stream_consumer.borrow().as_ref() {
|
||
stream_consumer.consume_chunk(chunk.as_slice());
|
||
} else if let Some(body) = self.body_stream.get() {
|
||
body.enqueue_native(chunk, can_gc);
|
||
}
|
||
}
|
||
|
||
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
|
||
pub(crate) fn finish(&self, can_gc: CanGc) {
|
||
if let Some(body) = self.body_stream.get() {
|
||
body.controller_close_native(can_gc);
|
||
}
|
||
let stream_consumer = self.stream_consumer.borrow_mut().take();
|
||
if let Some(stream_consumer) = stream_consumer {
|
||
stream_consumer.stream_end();
|
||
}
|
||
}
|
||
}
|