Skip to main content

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