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