/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/fetch/response.rs
555 строк
20 KB
Tim van der Lippe
script: Mechanically migrate to `reflect_dom_object_with_proto` (#46529)
15 июл 2026, 23:09
Не верифицирован
15 июл 2026, 23:09
2557009
Код
Авторство
О чём код?
/* 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::cell::DomRefCell; use script_bindings::cformat; use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto}; use servo_url::ServoUrl; use url::Position; use crate::body::{ BodyMixin, BodyType, Extractable, ExtractedBody, body_text_stream, clone_body_stream_for_dom_body, consume_body, }; 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; 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::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>, } impl Response { pub(crate) fn new_inherited(stream: DomRoot<ReadableStream>) -> Response { 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), } } /// <https://fetch.spec.whatwg.org/#dom-response> pub(crate) fn new(cx: &mut js::context::JSContext, global: &GlobalScope) -> DomRoot<Response> { Self::new_with_proto(cx, global, None) } fn new_with_proto( cx: &mut js::context::JSContext, global: &GlobalScope, proto: Option<HandleObject>, ) -> DomRoot<Response> { let stream = ReadableStream::new_with_external_underlying_source( cx, global, UnderlyingSourceType::FetchResponse, ) .expect("Failed to create ReadableStream with external underlying source"); reflect_dom_object_with_proto(cx, Box::new(Response::new_inherited(stream)), global, proto) } pub(crate) fn error_stream(&self, cx: &mut js::context::JSContext, error: Error) { if let Some(body) = self.fetch_body_stream.get() { body.error_native(cx, error); } } 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, cx: &mut js::context::JSContext) -> Vec<u8> { let headers = self.Headers(cx); 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( cx: &mut js::context::JSContext, global: &GlobalScope, proto: Option<HandleObject>, 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(cx, global, proto); // 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(cx).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(cx, global, false)?), None => None, }; // 5. Perform *initialize a response* given this, init, and bodyWithType. initialize_response(cx, body_with_type, init, response) } /// <https://fetch.spec.whatwg.org/#dom-response-error> fn Error(cx: &mut js::context::JSContext, global: &GlobalScope) -> DomRoot<Response> { let response = Response::new(cx, global); *response.response_type.borrow_mut() = DOMResponseType::Error; response.Headers(cx).set_guard(Guard::Immutable); *response.status.borrow_mut() = HttpStatus::new_error(); response } /// <https://fetch.spec.whatwg.org/#dom-response-redirect> fn Redirect( cx: &mut js::context::JSContext, global: &GlobalScope, url: USVString, status: u16, ) -> 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(cx, global); // 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(cx) .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(cx).set_guard(Guard::Immutable); // Step 7 Ok(response) } /// <https://fetch.spec.whatwg.org/#dom-response-json> fn CreateFromJson( cx: &mut js::context::JSContext, global: &GlobalScope, data: HandleValue, init: &ResponseBinding::ResponseInit, ) -> 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(cx, global, false)?; // 3. Let responseObject be the result of creating a Response object, given a new response, // "response", and the current realm. let response = Response::new(cx, global); response.Headers(cx).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(cx, 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 /// this’s response’s URL list’s 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, cx: &mut js::context::JSContext) -> DomRoot<Headers> { self.headers_reflector .or_init(|| Headers::for_response(cx, &self.global())) } /// <https://fetch.spec.whatwg.org/#dom-response-clone> fn Clone(&self, cx: &mut js::context::JSContext) -> 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 this’s response. let new_response = Response::new(cx, &self.global()); new_response .Headers(cx) .copy_from_headers(&self.Headers(cx))?; new_response .Headers(cx) .set_guard(self.Headers(cx).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()); // Step 3. Return the result of creating a Response object, // given clonedResponse, this’s headers’s guard, and this’s relevant realm. clone_body_stream_for_dom_body(cx, &self.body_stream, &new_response.body_stream)?; // 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_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, cx: &mut js::context::JSContext) -> Rc<Promise> { consume_body(cx, self, BodyType::Text) } /// <https://fetch.spec.whatwg.org/#dom-body-blob> fn Blob(&self, cx: &mut js::context::JSContext) -> Rc<Promise> { consume_body(cx, self, BodyType::Blob) } /// <https://fetch.spec.whatwg.org/#dom-body-formdata> fn FormData(&self, cx: &mut js::context::JSContext) -> Rc<Promise> { consume_body(cx, self, BodyType::FormData) } /// <https://fetch.spec.whatwg.org/#dom-body-json> fn Json(&self, cx: &mut js::context::JSContext) -> Rc<Promise> { consume_body(cx, self, BodyType::Json) } /// <https://fetch.spec.whatwg.org/#dom-body-arraybuffer> fn ArrayBuffer(&self, cx: &mut js::context::JSContext) -> Rc<Promise> { consume_body(cx, self, BodyType::ArrayBuffer) } /// <https://fetch.spec.whatwg.org/#dom-body-bytes> fn Bytes(&self, cx: &mut js::context::JSContext) -> Rc<Promise> { consume_body(cx, self, BodyType::Bytes) } /// <https://fetch.spec.whatwg.org/#dom-body-textstream> fn TextStream(&self, cx: &mut js::context::JSContext) -> Fallible<DomRoot<ReadableStream>> { body_text_stream(cx, self) } } /// <https://fetch.spec.whatwg.org/#initialize-a-response> fn initialize_response( cx: &mut js::context::JSContext, 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 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(cx).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( c"Body is non-null but init's status member is a null body status".to_owned(), )); }; // 6.2 Set response’s body to body’s body. response.body_stream.set(Some(&*body.stream)); response.fetch_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 && !response .Headers(cx) .Has(ByteString::new(b"Content-Type".to_vec())) .unwrap() { response.Headers(cx).Append( ByteString::new(b"Content-Type".to_vec()), ByteString::new(content_type_contents.as_bytes().to_vec()), )?; }; } else { response.body_stream.set(None); response.fetch_body_stream.set(None); } Ok(response) } fn serialize_without_fragment(url: &ServoUrl) -> &str { &url[..Position::AfterQuery] } impl Response { pub(crate) fn set_type( &self, cx: &mut js::context::JSContext, new_response_type: DOMResponseType, ) { *self.response_type.borrow_mut() = new_response_type; self.set_response_members_by_type(cx, new_response_type); } pub(crate) fn set_headers( &self, cx: &mut js::context::JSContext, option_hyper_headers: Option<Serde<HyperHeaders>>, ) { self.Headers(cx).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, cx: &mut js::context::JSContext, response_type: DOMResponseType, ) { match response_type { DOMResponseType::Error => { *self.status.borrow_mut() = HttpStatus::new_error(); self.set_headers(cx, None); }, DOMResponseType::Opaque => { *self.url_list.borrow_mut() = vec![]; *self.status.borrow_mut() = HttpStatus::new_error(); self.set_headers(cx, None); self.body_stream.set(None); self.fetch_body_stream.set(None); }, DOMResponseType::Opaqueredirect => { *self.status.borrow_mut() = HttpStatus::new_error(); self.set_headers(cx, None); 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, cx: &mut js::context::JSContext, chunk: Vec<u8>) { // 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(cx, chunk); } } pub(crate) fn finish(&self, cx: &mut js::context::JSContext) { if let Some(body) = self.fetch_body_stream.get() { body.controller_close_native(cx); } let stream_consumer = self.stream_consumer.borrow_mut().take(); if let Some(stream_consumer) = stream_consumer { stream_consumer.stream_end(); } } }