retina_fetch/
response.rs

1// Copyright (C) 2023 Tristan Gerritsen <tristan@thewoosh.org>
2// All Rights Reserved.
3
4use std::{sync::Arc, io::BufRead};
5
6use futures_core::Stream;
7use hyper::body::{Buf, Bytes};
8use log::error;
9use url::Url;
10
11use crate::{Request, StatusCode};
12
13type Inner = hyper::Response<hyper::Body>;
14
15/// Represents the HTTP response to an [request][Request].
16#[derive(Debug)]
17pub struct Response {
18    request: Arc<Request>,
19    inner: Inner,
20}
21
22impl Response {
23    pub(crate) fn new_about(request: Arc<Request>, body: &'static str) -> Self {
24        Self {
25            request,
26            inner: Inner::new(body.into()),
27        }
28    }
29
30    pub(crate) fn new_file<S, O, E>(
31        request: Arc<Request>,
32        stream: S,
33    ) -> Self
34        where S: Stream<Item = Result<O, E>> + Send + 'static,
35            O: Into<Bytes> + 'static,
36            E: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
37        {
38        Self {
39            request,
40            inner: Inner::new(hyper::Body::wrap_stream(stream))
41        }
42    }
43
44    /// Get the [`Content-Type`][spec] header, which specifies which [media type]
45    /// this response is represented with.
46    ///
47    /// [spec]: https://www.rfc-editor.org/rfc/rfc2046.html
48    /// [media type]: https://httpwg.org/specs/rfc9110.html#field.content-type
49    pub fn content_type(&self) -> mime::Mime {
50        let Some(content_type) = self.inner.headers().get(hyper::header::CONTENT_TYPE) else {
51            return mime::APPLICATION_OCTET_STREAM;
52        };
53
54        let Ok(content_type) = content_type.to_str() else {
55            return mime::APPLICATION_OCTET_STREAM;
56        };
57
58        content_type.parse().unwrap_or(mime::APPLICATION_OCTET_STREAM)
59    }
60
61    /// Gets the redirect location this response points to, if the response is
62    /// a redirection.
63    pub fn redirect_location(&self) -> Option<&str> {
64        if !self.status().is_redirection() {
65            return None;
66        }
67
68        let location = self.inner.headers().get(hyper::header::LOCATION)?;
69        location.to_str().ok()
70    }
71
72    /// Gets the redirect location this response points to, if the response is
73    /// a redirection.
74    pub fn redirect_url(&self) -> Option<Url> {
75        let location = self.redirect_location()?;
76        match url::Url::parse(location) {
77            Ok(url) => Some(url),
78            Err(e) => {
79                error!("Redirect status ({:?}) with invalid URL: \"{location}\", error: {e}", self.status());
80                None
81            }
82        }
83    }
84
85    /// Get the body of this response.
86    pub async fn body(&mut self) -> Box<dyn BufRead + '_> {
87        Box::new(hyper::body::aggregate(self.inner.body_mut()).await.unwrap().reader())
88    }
89
90    /// TODO: this function is not needed if the BufRead can be somehow
91    /// [`Seek`][std::io::Seek].
92    pub async fn body_bytes(&mut self) -> Bytes {
93        hyper::body::to_bytes(self.inner.body_mut()).await.unwrap()
94    }
95
96    /// Get the [`Request`] that created this [`Response`].
97    pub fn request(&self) -> &Request {
98        &self.request
99    }
100
101    /// Check if this response is successful.
102    pub fn ok(&self) -> bool {
103        self.status().is_successful()
104    }
105
106    /// Get the [StatusCode] of this response.
107    pub fn status(&self) -> StatusCode {
108        self.inner.status().into()
109    }
110
111    /// Get the [URL][Url] this response was requested with.
112    pub fn url(&self) -> &Url {
113        &self.request.url
114    }
115}
116
117impl From<(Arc<Request>, Inner)> for Response {
118    fn from(value: (Arc<Request>, Inner)) -> Self {
119        let (request, inner) = value;
120        Self { request, inner }
121    }
122}