Skip to main content

script/dom/
response.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::rc::Rc;
7use std::str::FromStr;
8
9use dom_struct::dom_struct;
10use http::header::HeaderMap as HyperHeaders;
11use hyper_serde::Serde;
12use js::rust::{HandleObject, HandleValue};
13use net_traits::http_status::HttpStatus;
14use script_bindings::cell::DomRefCell;
15use script_bindings::cformat;
16use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto_and_cx};
17use servo_url::ServoUrl;
18use url::Position;
19
20use crate::body::{
21    BodyMixin, BodyType, Extractable, ExtractedBody, clone_body_stream_for_dom_body, consume_body,
22};
23use crate::dom::bindings::codegen::Bindings::HeadersBinding::HeadersMethods;
24use crate::dom::bindings::codegen::Bindings::ResponseBinding;
25use crate::dom::bindings::codegen::Bindings::ResponseBinding::{
26    ResponseMethods, ResponseType as DOMResponseType,
27};
28use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
29use crate::dom::bindings::error::{Error, Fallible};
30use crate::dom::bindings::reflector::DomGlobal;
31use crate::dom::bindings::root::{DomRoot, MutNullableDom};
32use crate::dom::bindings::str::{ByteString, USVString, serialize_jsval_to_json_utf8};
33use crate::dom::globalscope::GlobalScope;
34use crate::dom::headers::{Guard, Headers, is_obs_text, is_vchar};
35use crate::dom::promise::Promise;
36use crate::dom::stream::readablestream::ReadableStream;
37use crate::dom::stream::underlyingsourcecontainer::UnderlyingSourceType;
38use crate::script_runtime::{CanGc, StreamConsumer};
39
40#[dom_struct]
41pub(crate) struct Response {
42    reflector_: Reflector,
43    headers_reflector: MutNullableDom<Headers>,
44    #[no_trace]
45    status: DomRefCell<HttpStatus>,
46    response_type: DomRefCell<DOMResponseType>,
47    /// <https://fetch.spec.whatwg.org/#concept-response-url>
48    /// FIXME: This should always point to the last entry of `url_list`.
49    /// Somehow we directly set it instead.
50    #[no_trace]
51    url: DomRefCell<Option<ServoUrl>>,
52    #[no_trace]
53    url_list: DomRefCell<Vec<ServoUrl>>,
54    /// The stream of <https://fetch.spec.whatwg.org/#body>.
55    body_stream: MutNullableDom<ReadableStream>,
56    /// The stream that receives network delivered bytes for Fetch responses.
57    /// This must remain stable even if `body_stream` is replaced by `tee()` branches during `clone()`.
58    fetch_body_stream: MutNullableDom<ReadableStream>,
59    #[ignore_malloc_size_of = "StreamConsumer"]
60    stream_consumer: DomRefCell<Option<StreamConsumer>>,
61    /// FIXME: This should be removed.
62    redirected: Cell<bool>,
63    is_body_empty: Cell<bool>,
64}
65
66impl Response {
67    pub(crate) fn new_inherited(cx: &mut js::context::JSContext, global: &GlobalScope) -> Response {
68        let stream = ReadableStream::new_with_external_underlying_source(
69            cx,
70            global,
71            UnderlyingSourceType::FetchResponse,
72        )
73        .expect("Failed to create ReadableStream with external underlying source");
74        Response {
75            reflector_: Reflector::new(),
76            headers_reflector: Default::default(),
77            status: DomRefCell::new(HttpStatus::default()),
78            response_type: DomRefCell::new(DOMResponseType::Default),
79            url: DomRefCell::new(None),
80            url_list: DomRefCell::new(vec![]),
81            body_stream: MutNullableDom::new(Some(&*stream)),
82            fetch_body_stream: MutNullableDom::new(Some(&*stream)),
83            stream_consumer: DomRefCell::new(None),
84            redirected: Cell::new(false),
85            is_body_empty: Cell::new(true),
86        }
87    }
88
89    /// <https://fetch.spec.whatwg.org/#dom-response>
90    pub(crate) fn new(cx: &mut js::context::JSContext, global: &GlobalScope) -> DomRoot<Response> {
91        Self::new_with_proto(cx, global, None)
92    }
93
94    fn new_with_proto(
95        cx: &mut js::context::JSContext,
96        global: &GlobalScope,
97        proto: Option<HandleObject>,
98    ) -> DomRoot<Response> {
99        reflect_dom_object_with_proto_and_cx(
100            Box::new(Response::new_inherited(cx, global)),
101            global,
102            proto,
103            cx,
104        )
105    }
106
107    pub(crate) fn error_stream(&self, cx: &mut js::context::JSContext, error: Error) {
108        if let Some(body) = self.fetch_body_stream.get() {
109            body.error_native(cx, error);
110        }
111    }
112
113    pub(crate) fn is_disturbed(&self) -> bool {
114        let body_stream = self.body_stream.get();
115        body_stream
116            .as_ref()
117            .is_some_and(|stream| stream.is_disturbed())
118    }
119
120    pub(crate) fn is_locked(&self) -> bool {
121        let body_stream = self.body_stream.get();
122        body_stream
123            .as_ref()
124            .is_some_and(|stream| stream.is_locked())
125    }
126}
127
128impl BodyMixin for Response {
129    fn is_body_used(&self) -> bool {
130        self.is_disturbed()
131    }
132
133    fn is_unusable(&self) -> bool {
134        self.body_stream
135            .get()
136            .is_some_and(|stream| stream.is_disturbed() || stream.is_locked())
137    }
138
139    fn body(&self) -> Option<DomRoot<ReadableStream>> {
140        self.body_stream.get()
141    }
142
143    fn get_mime_type(&self, cx: &mut js::context::JSContext) -> Vec<u8> {
144        let headers = self.Headers(CanGc::from_cx(cx));
145        headers.extract_mime_type()
146    }
147}
148
149/// <https://fetch.spec.whatwg.org/#redirect-status>
150fn is_redirect_status(status: u16) -> bool {
151    status == 301 || status == 302 || status == 303 || status == 307 || status == 308
152}
153
154/// <https://tools.ietf.org/html/rfc7230#section-3.1.2>
155fn is_valid_status_text(status_text: &ByteString) -> bool {
156    // reason-phrase  = *( HTAB / SP / VCHAR / obs-text )
157    for byte in status_text.iter() {
158        if !(*byte == b'\t' || *byte == b' ' || is_vchar(*byte) || is_obs_text(*byte)) {
159            return false;
160        }
161    }
162    true
163}
164
165/// <https://fetch.spec.whatwg.org/#null-body-status>
166fn is_null_body_status(status: u16) -> bool {
167    status == 101 || status == 204 || status == 205 || status == 304
168}
169
170impl ResponseMethods<crate::DomTypeHolder> for Response {
171    /// <https://fetch.spec.whatwg.org/#dom-response>
172    fn Constructor(
173        cx: &mut js::context::JSContext,
174        global: &GlobalScope,
175        proto: Option<HandleObject>,
176        body_init: Option<BodyInit>,
177        init: &ResponseBinding::ResponseInit,
178    ) -> Fallible<DomRoot<Response>> {
179        // 1. Set this’s response to a new response.
180        // Our Response/Body types don't actually hold onto an internal fetch Response.
181        let response = Response::new_with_proto(cx, global, proto);
182        if body_init.is_some() {
183            response.is_body_empty.set(false);
184        }
185
186        // 2. Set this’s headers to a new Headers object with this’s relevant realm,
187        // whose header list is this’s response’s header list and guard is "response".
188        response
189            .Headers(CanGc::from_cx(cx))
190            .set_guard(Guard::Response);
191
192        // 3. Let bodyWithType be null.
193        // 4. If body is non-null, then set bodyWithType to the result of extracting body.
194        let body_with_type = match body_init {
195            Some(body) => Some(body.extract(cx, global, false)?),
196            None => None,
197        };
198
199        // 5. Perform *initialize a response* given this, init, and bodyWithType.
200        initialize_response(cx, global, body_with_type, init, response)
201    }
202
203    /// <https://fetch.spec.whatwg.org/#dom-response-error>
204    fn Error(cx: &mut js::context::JSContext, global: &GlobalScope) -> DomRoot<Response> {
205        let response = Response::new(cx, global);
206        *response.response_type.borrow_mut() = DOMResponseType::Error;
207        response
208            .Headers(CanGc::from_cx(cx))
209            .set_guard(Guard::Immutable);
210        *response.status.borrow_mut() = HttpStatus::new_error();
211        response
212    }
213
214    /// <https://fetch.spec.whatwg.org/#dom-response-redirect>
215    fn Redirect(
216        cx: &mut js::context::JSContext,
217        global: &GlobalScope,
218        url: USVString,
219        status: u16,
220    ) -> Fallible<DomRoot<Response>> {
221        // Step 1
222        let base_url = global.api_base_url();
223        let parsed_url = base_url.join(&url.0);
224
225        // Step 2
226        let url = match parsed_url {
227            Ok(url) => url,
228            Err(_) => return Err(Error::Type(c"ServoUrl could not be parsed".to_owned())),
229        };
230
231        // Step 3
232        if !is_redirect_status(status) {
233            return Err(Error::Range(c"status is not a redirect status".to_owned()));
234        }
235
236        // Step 4
237        // see Step 4 continued
238        let response = Response::new(cx, global);
239
240        // Step 5
241        *response.status.borrow_mut() = HttpStatus::new_raw(status, vec![]);
242
243        // Step 6
244        let url_bytestring =
245            ByteString::from_str(url.as_str()).unwrap_or(ByteString::new(b"".to_vec()));
246        response
247            .Headers(CanGc::from_cx(cx))
248            .Set(ByteString::new(b"Location".to_vec()), url_bytestring)?;
249
250        // Step 4 continued
251        // Headers Guard is set to Immutable here to prevent error in Step 6
252        response
253            .Headers(CanGc::from_cx(cx))
254            .set_guard(Guard::Immutable);
255
256        // Step 7
257        Ok(response)
258    }
259
260    /// <https://fetch.spec.whatwg.org/#dom-response-json>
261    fn CreateFromJson(
262        cx: &mut js::context::JSContext,
263        global: &GlobalScope,
264        data: HandleValue,
265        init: &ResponseBinding::ResponseInit,
266    ) -> Fallible<DomRoot<Response>> {
267        // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
268        let json_str = serialize_jsval_to_json_utf8(cx.into(), data)?;
269
270        // 2. Let body be the result of extracting bytes
271        // The spec's definition of JSON bytes is a UTF-8 encoding so using a DOMString here handles
272        // the encoding part.
273        let body_init = BodyInit::String(json_str);
274        let mut body = body_init.extract(cx, global, false)?;
275
276        // 3. Let responseObject be the result of creating a Response object, given a new response,
277        // "response", and the current realm.
278        let response = Response::new(cx, global);
279        response
280            .Headers(CanGc::from_cx(cx))
281            .set_guard(Guard::Response);
282
283        // 4. Perform initialize a response given responseObject, init, and (body, "application/json").
284        body.content_type = Some("application/json".into());
285        initialize_response(cx, global, Some(body), init, response)
286    }
287
288    /// <https://fetch.spec.whatwg.org/#dom-response-type>
289    fn Type(&self) -> DOMResponseType {
290        *self.response_type.borrow() // into()
291    }
292
293    /// <https://fetch.spec.whatwg.org/#dom-response-url>
294    fn Url(&self) -> USVString {
295        USVString(String::from(
296            (*self.url.borrow())
297                .as_ref()
298                .map(serialize_without_fragment)
299                .unwrap_or(""),
300        ))
301    }
302
303    /// <https://fetch.spec.whatwg.org/#dom-response-redirected>
304    /// TODO: The redirected getter steps are to return true if
305    /// this’s response’s URL list’s size is greater than 1; otherwise false.
306    ///
307    /// But if we do like spec says, test fails, probably because
308    /// we not fully set URL list in spec steps.
309    fn Redirected(&self) -> bool {
310        self.redirected.get()
311    }
312
313    /// <https://fetch.spec.whatwg.org/#dom-response-status>
314    fn Status(&self) -> u16 {
315        self.status.borrow().raw_code()
316    }
317
318    /// <https://fetch.spec.whatwg.org/#dom-response-ok>
319    fn Ok(&self) -> bool {
320        self.status.borrow().is_success()
321    }
322
323    /// <https://fetch.spec.whatwg.org/#dom-response-statustext>
324    fn StatusText(&self) -> ByteString {
325        ByteString::new(self.status.borrow().message().to_vec())
326    }
327
328    /// <https://fetch.spec.whatwg.org/#dom-response-headers>
329    fn Headers(&self, can_gc: CanGc) -> DomRoot<Headers> {
330        self.headers_reflector
331            .or_init(|| Headers::for_response(&self.global(), can_gc))
332    }
333
334    /// <https://fetch.spec.whatwg.org/#dom-response-clone>
335    fn Clone(&self, cx: &mut js::context::JSContext) -> Fallible<DomRoot<Response>> {
336        // Step 1. If this is unusable, then throw a TypeError.
337        if self.is_unusable() {
338            return Err(Error::Type(c"cannot clone a disturbed response".to_owned()));
339        }
340
341        // Step 2. Let clonedResponse be the result of cloning this’s response.
342        let new_response = Response::new(cx, &self.global());
343        new_response
344            .Headers(CanGc::from_cx(cx))
345            .copy_from_headers(self.Headers(CanGc::from_cx(cx)))?;
346        new_response
347            .Headers(CanGc::from_cx(cx))
348            .set_guard(self.Headers(CanGc::from_cx(cx)).get_guard());
349
350        *new_response.response_type.borrow_mut() = *self.response_type.borrow();
351        new_response
352            .status
353            .borrow_mut()
354            .clone_from(&self.status.borrow());
355        new_response.url.borrow_mut().clone_from(&self.url.borrow());
356        new_response
357            .url_list
358            .borrow_mut()
359            .clone_from(&self.url_list.borrow());
360        new_response.is_body_empty.set(self.is_body_empty.get());
361
362        // Step 3. Return the result of creating a Response object,
363        // given clonedResponse, this’s headers’s guard, and this’s relevant realm.
364        clone_body_stream_for_dom_body(cx, &self.body_stream, &new_response.body_stream)?;
365        // The cloned response must not receive network chunks directly; it is fed via the tee branch.
366        new_response.fetch_body_stream.set(None);
367
368        Ok(new_response)
369    }
370
371    /// <https://fetch.spec.whatwg.org/#dom-body-bodyused>
372    fn BodyUsed(&self) -> bool {
373        !self.is_body_empty.get() && self.is_body_used()
374    }
375
376    /// <https://fetch.spec.whatwg.org/#dom-body-body>
377    fn GetBody(&self) -> Option<DomRoot<ReadableStream>> {
378        self.body()
379    }
380
381    /// <https://fetch.spec.whatwg.org/#dom-body-text>
382    fn Text(&self, cx: &mut js::context::JSContext) -> Rc<Promise> {
383        consume_body(cx, self, BodyType::Text)
384    }
385
386    /// <https://fetch.spec.whatwg.org/#dom-body-blob>
387    fn Blob(&self, cx: &mut js::context::JSContext) -> Rc<Promise> {
388        consume_body(cx, self, BodyType::Blob)
389    }
390
391    /// <https://fetch.spec.whatwg.org/#dom-body-formdata>
392    fn FormData(&self, cx: &mut js::context::JSContext) -> Rc<Promise> {
393        consume_body(cx, self, BodyType::FormData)
394    }
395
396    /// <https://fetch.spec.whatwg.org/#dom-body-json>
397    fn Json(&self, cx: &mut js::context::JSContext) -> Rc<Promise> {
398        consume_body(cx, self, BodyType::Json)
399    }
400
401    /// <https://fetch.spec.whatwg.org/#dom-body-arraybuffer>
402    fn ArrayBuffer(&self, cx: &mut js::context::JSContext) -> Rc<Promise> {
403        consume_body(cx, self, BodyType::ArrayBuffer)
404    }
405
406    /// <https://fetch.spec.whatwg.org/#dom-body-bytes>
407    fn Bytes(&self, cx: &mut js::context::JSContext) -> Rc<Promise> {
408        consume_body(cx, self, BodyType::Bytes)
409    }
410}
411
412/// <https://fetch.spec.whatwg.org/#initialize-a-response>
413fn initialize_response(
414    cx: &mut js::context::JSContext,
415    global: &GlobalScope,
416    body: Option<ExtractedBody>,
417    init: &ResponseBinding::ResponseInit,
418    response: DomRoot<Response>,
419) -> Result<DomRoot<Response>, Error> {
420    // 1. If init["status"] is not in the range 200 to 599, inclusive, then throw a RangeError.
421    if init.status < 200 || init.status > 599 {
422        return Err(Error::Range(cformat!(
423            "init's status member should be in the range 200 to 599, inclusive, but is {}",
424            init.status
425        )));
426    }
427
428    // 2. If init["statusText"] is not the empty string and does not match the reason-phrase token production,
429    // then throw a TypeError.
430    if !is_valid_status_text(&init.statusText) {
431        return Err(Error::Type(
432            c"init's statusText member does not match the reason-phrase token production"
433                .to_owned(),
434        ));
435    }
436
437    // 3. Set response’s response’s status to init["status"].
438    // 4. Set response’s response’s status message to init["statusText"].
439    *response.status.borrow_mut() =
440        HttpStatus::new_raw(init.status, init.statusText.clone().into());
441
442    // 5. If init["headers"] exists, then fill response’s headers with init["headers"].
443    if let Some(ref headers_member) = init.headers {
444        response
445            .Headers(CanGc::from_cx(cx))
446            .fill(Some(headers_member.clone()))?;
447    }
448
449    // 6. If body is non-null, then:
450    if let Some(ref body) = body {
451        // 6.1 If response’s status is a null body status, then throw a TypeError.
452        if is_null_body_status(init.status) {
453            return Err(Error::Type(
454                c"Body is non-null but init's status member is a null body status".to_owned(),
455            ));
456        };
457
458        // 6.2 Set response’s body to body’s body.
459        response.body_stream.set(Some(&*body.stream));
460        response.fetch_body_stream.set(Some(&*body.stream));
461        response.is_body_empty.set(false);
462
463        // 6.3 If body’s type is non-null and response’s header list does not contain `Content-Type`,
464        // then append (`Content-Type`, body’s type) to response’s header list.
465        if let Some(content_type_contents) = &body.content_type &&
466            !response
467                .Headers(CanGc::from_cx(cx))
468                .Has(ByteString::new(b"Content-Type".to_vec()))
469                .unwrap()
470        {
471            response.Headers(CanGc::from_cx(cx)).Append(
472                ByteString::new(b"Content-Type".to_vec()),
473                ByteString::new(content_type_contents.as_bytes().to_vec()),
474            )?;
475        };
476    } else {
477        // Reset FetchResponse to an in-memory stream with empty byte sequence here for
478        // no-init-body case. This is because the Response/Body types here do not hold onto a
479        // fetch Response object.
480        let stream = ReadableStream::new_from_bytes(cx, global, Vec::with_capacity(0))?;
481        response.body_stream.set(Some(&*stream));
482        response.fetch_body_stream.set(Some(&*stream));
483    }
484
485    Ok(response)
486}
487
488fn serialize_without_fragment(url: &ServoUrl) -> &str {
489    &url[..Position::AfterQuery]
490}
491
492impl Response {
493    pub(crate) fn set_type(&self, new_response_type: DOMResponseType, can_gc: CanGc) {
494        *self.response_type.borrow_mut() = new_response_type;
495        self.set_response_members_by_type(new_response_type, can_gc);
496    }
497
498    pub(crate) fn set_headers(
499        &self,
500        option_hyper_headers: Option<Serde<HyperHeaders>>,
501        can_gc: CanGc,
502    ) {
503        self.Headers(can_gc)
504            .set_headers(match option_hyper_headers {
505                Some(hyper_headers) => hyper_headers.into_inner(),
506                None => HyperHeaders::new(),
507            });
508    }
509
510    pub(crate) fn set_status(&self, status: &HttpStatus) {
511        self.status.borrow_mut().clone_from(status);
512    }
513
514    pub(crate) fn set_final_url(&self, final_url: ServoUrl) {
515        *self.url.borrow_mut() = Some(final_url);
516    }
517
518    pub(crate) fn set_redirected(&self, is_redirected: bool) {
519        self.redirected.set(is_redirected);
520    }
521
522    fn set_response_members_by_type(&self, response_type: DOMResponseType, can_gc: CanGc) {
523        match response_type {
524            DOMResponseType::Error => {
525                *self.status.borrow_mut() = HttpStatus::new_error();
526                self.set_headers(None, can_gc);
527            },
528            DOMResponseType::Opaque => {
529                *self.url_list.borrow_mut() = vec![];
530                *self.status.borrow_mut() = HttpStatus::new_error();
531                self.set_headers(None, can_gc);
532                self.body_stream.set(None);
533                self.fetch_body_stream.set(None);
534            },
535            DOMResponseType::Opaqueredirect => {
536                *self.status.borrow_mut() = HttpStatus::new_error();
537                self.set_headers(None, can_gc);
538                self.body_stream.set(None);
539                self.fetch_body_stream.set(None);
540            },
541            DOMResponseType::Default => {},
542            DOMResponseType::Basic => {},
543            DOMResponseType::Cors => {},
544        }
545    }
546
547    pub(crate) fn set_stream_consumer(&self, sc: Option<StreamConsumer>) {
548        *self.stream_consumer.borrow_mut() = sc;
549    }
550
551    pub(crate) fn stream_chunk(&self, cx: &mut js::context::JSContext, chunk: Vec<u8>) {
552        self.is_body_empty.set(false);
553        // Note, are these two actually mutually exclusive?
554        if let Some(stream_consumer) = self.stream_consumer.borrow().as_ref() {
555            stream_consumer.consume_chunk(chunk.as_slice());
556        } else if let Some(body) = self.fetch_body_stream.get() {
557            body.enqueue_native(cx, chunk);
558        }
559    }
560
561    pub(crate) fn finish(&self, cx: &mut js::context::JSContext) {
562        if let Some(body) = self.fetch_body_stream.get() {
563            body.controller_close_native(cx);
564        }
565        let stream_consumer = self.stream_consumer.borrow_mut().take();
566        if let Some(stream_consumer) = stream_consumer {
567            stream_consumer.stream_end();
568        }
569    }
570}