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