Files
servo/components/script/dom/response.rs
Euclid Ye 5d91793d1c net: Fix link, reduce visibility, add TODO for Response::url (#44274)
I wanted to follow up to #43987, but don't know how to proceed
meaningfully :(

- Reduce visibility
- Add TODO for `Response::url`. This should be removed and determined by
`url_list` instead.
- Fix some spec link

Testing: Nothing should change as no code change.

---------

Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
2026-04-18 01:02:48 +00:00

564 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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::cell::Cell;
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 script_bindings::cformat;
use servo_url::ServoUrl;
use url::Position;
use crate::body::{
BodyMixin, BodyType, Extractable, ExtractedBody, clone_body_stream_for_dom_body, 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::stream::readablestream::ReadableStream;
use crate::dom::stream::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>,
/// <https://fetch.spec.whatwg.org/#concept-response-url>
/// FIXME: This should always point to the last entry of `url_list`.
/// Somehow we directly set it instead.
#[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>,
/// The stream that receives network delivered bytes for Fetch responses.
/// This must remain stable even if `body_stream` is replaced by `tee()` branches during `clone()`.
fetch_body_stream: MutNullableDom<ReadableStream>,
#[ignore_malloc_size_of = "StreamConsumer"]
stream_consumer: DomRefCell<Option<StreamConsumer>>,
/// FIXME: This should be removed.
redirected: Cell<bool>,
is_body_empty: Cell<bool>,
}
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)),
fetch_body_stream: MutNullableDom::new(Some(&*stream)),
stream_consumer: DomRefCell::new(None),
redirected: Cell::new(false),
is_body_empty: Cell::new(true),
}
}
/// <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.fetch_body_stream.get() {
body.error_native(error, can_gc);
}
}
pub(crate) fn is_disturbed(&self) -> bool {
let body_stream = self.body_stream.get();
body_stream
.as_ref()
.is_some_and(|stream| stream.is_disturbed())
}
pub(crate) fn is_locked(&self) -> bool {
let body_stream = self.body_stream.get();
body_stream
.as_ref()
.is_some_and(|stream| stream.is_locked())
}
}
impl BodyMixin for Response {
fn is_body_used(&self) -> bool {
self.is_disturbed()
}
fn is_unusable(&self) -> bool {
self.body_stream
.get()
.is_some_and(|stream| stream.is_disturbed() || 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 thiss 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);
if body_init.is_some() {
response.is_body_empty.set(false);
}
// 2. Set thiss headers to a new Headers object with thiss relevant realm,
// whose header list is thiss responses 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, false, 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(c"ServoUrl could not be parsed".to_owned())),
};
// Step 3
if !is_redirect_status(status) {
return Err(Error::Range(c"status is not a redirect status".to_owned()));
}
// 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>
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, false, 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>
/// TODO: The redirected getter steps are to return true if
/// thiss responses URL lists size is greater than 1; otherwise false.
///
/// But if we do like spec says, test fails, probably because
/// we not fully set URL list in spec steps.
fn Redirected(&self) -> bool {
self.redirected.get()
}
/// <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 this is unusable, then throw a TypeError.
if self.is_unusable() {
return Err(Error::Type(c"cannot clone a disturbed response".to_owned()));
}
// Step 2. Let clonedResponse be the result of cloning thiss response.
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());
*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());
new_response.is_body_empty.set(self.is_body_empty.get());
// Step 3. Return the result of creating a Response object,
// given clonedResponse, thiss headerss guard, and thiss relevant realm.
clone_body_stream_for_dom_body(&self.body_stream, &new_response.body_stream, can_gc)?;
// The cloned response must not receive network chunks directly; it is fed via the tee branch.
new_response.fetch_body_stream.set(None);
Ok(new_response)
}
/// <https://fetch.spec.whatwg.org/#dom-body-bodyused>
fn BodyUsed(&self) -> bool {
!self.is_body_empty.get() && self.is_body_used()
}
/// <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(cformat!(
"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(
c"init's statusText member does not match the reason-phrase token production"
.to_owned(),
));
}
// 3. Set responses responses status to init["status"].
// 4. Set responses responses 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 responses 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 responses status is a null body status, then throw a TypeError.
if is_null_body_status(init.status) {
return Err(Error::Type(
c"Body is non-null but init's status member is a null body status".to_owned(),
));
};
// 6.2 Set responses body to bodys body.
response.body_stream.set(Some(&*body.stream));
response.fetch_body_stream.set(Some(&*body.stream));
response.is_body_empty.set(false);
// 6.3 If bodys type is non-null and responses header list does not contain `Content-Type`,
// then append (`Content-Type`, bodys type) to responses 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));
response.fetch_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.set(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);
self.fetch_body_stream.set(None);
},
DOMResponseType::Opaqueredirect => {
*self.status.borrow_mut() = HttpStatus::new_error();
self.set_headers(None, can_gc);
self.body_stream.set(None);
self.fetch_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) {
self.is_body_empty.set(false);
// 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.fetch_body_stream.get() {
body.enqueue_native(chunk, can_gc);
}
}
pub(crate) fn finish(&self, can_gc: CanGc) {
if let Some(body) = self.fetch_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();
}
}
}