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