Skip to main content

security_client_rs/http/
response.rs

1use std::{fmt, marker::PhantomData};
2
3use bytes::Bytes;
4use futures_util::{Stream, StreamExt};
5use http::{HeaderMap, StatusCode};
6use serde::de::DeserializeOwned;
7use serde_json::Value;
8use url::Url;
9
10use crate::{Error, Result, TransportError, error::DecodeError};
11
12/// Marker for endpoints that answer without a body.
13pub enum Empty {}
14
15/// Marker for endpoints that return non-JSON bytes, such as NDJSON exports,
16/// YAML policies and diagnostic archives.
17pub enum Raw {}
18
19/// A successful response. `T` is the type [`json`](Self::json) decodes into.
20///
21/// Non-success statuses never reach this type; [`send`](super::Transport::send)
22/// turns them into [`Error::Api`]. A failure while reading the body keeps the
23/// status and headers: [`Error::Body`] for an interrupted body and
24/// [`Error::ResponseTooLarge`] above the transport limit.
25///
26/// The body is read only when a consuming method is called. Choose one reader;
27/// capture any headers needed afterward before consuming the response. A successful
28/// HTTP status alone does not establish that the body is valid or an import fully succeeded.
29pub struct Response<T = Value> {
30    inner: reqwest::Response,
31    limit: usize,
32    operation: &'static str,
33    #[cfg(feature = "tracing")]
34    trace: Option<super::trace::Request>,
35    kind: PhantomData<fn() -> T>,
36}
37
38impl<T> Response<T> {
39    pub(crate) fn new(inner: reqwest::Response, limit: usize) -> Self {
40        Self {
41            inner,
42            limit,
43            operation: "request",
44            #[cfg(feature = "tracing")]
45            trace: None,
46            kind: PhantomData,
47        }
48    }
49
50    pub(crate) fn named(mut self, operation: &'static str) -> Self {
51        self.operation = operation;
52        self
53    }
54
55    #[cfg(feature = "tracing")]
56    pub(crate) fn traced(mut self, trace: super::trace::Request) -> Self {
57        self.trace = Some(trace);
58        self
59    }
60
61    pub(crate) fn cast<U>(self) -> Response<U> {
62        Response {
63            inner: self.inner,
64            limit: self.limit,
65            operation: self.operation,
66            #[cfg(feature = "tracing")]
67            trace: self.trace,
68            kind: PhantomData,
69        }
70    }
71
72    pub fn status_code(&self) -> StatusCode {
73        self.inner.status()
74    }
75
76    pub fn headers(&self) -> &HeaderMap {
77        self.inner.headers()
78    }
79
80    pub fn content_length(&self) -> Option<u64> {
81        self.inner.content_length()
82    }
83
84    /// The requested URL, including query values. `Debug` omits them.
85    pub fn url(&self) -> &Url {
86        self.inner.url()
87    }
88
89    /// Reads the whole body, failing with [`Error::ResponseTooLarge`] above the transport limit.
90    pub async fn bytes(mut self) -> Result<Bytes> {
91        let status = self.inner.status();
92        let headers = std::mem::take(self.inner.headers_mut());
93        let body = read_bounded(self.inner, self.limit).await;
94        let result = match body {
95            Bounded {
96                error: Some(source),
97                bytes,
98                ..
99            } => Err(Error::Body {
100                status,
101                headers: Box::new(headers),
102                received: bytes.len(),
103                source,
104            }),
105            Bounded {
106                truncated: true, ..
107            } => Err(Error::ResponseTooLarge {
108                limit: self.limit,
109                status,
110                headers: Box::new(headers),
111            }),
112            Bounded { bytes, .. } => Ok(Bytes::from(bytes)),
113        };
114        #[cfg(feature = "tracing")]
115        if let Some(trace) = &self.trace {
116            trace.body_finished(status, &result);
117        }
118        result
119    }
120
121    /// Reads the body as UTF-8, replacing invalid sequences.
122    pub async fn text(self) -> Result<String> {
123        Ok(String::from_utf8_lossy(&self.bytes().await?).into_owned())
124    }
125
126    /// Streams the body without the transport's size limit. Failures return
127    /// [`Error::Body`] with the status, headers and number of bytes delivered
128    /// before the failure. No body trace event is emitted.
129    pub fn bytes_stream(mut self) -> impl Stream<Item = Result<Bytes>> + Send {
130        let status = self.inner.status();
131        let headers = std::mem::take(self.inner.headers_mut());
132        let mut received = 0_usize;
133        self.inner.bytes_stream().map(move |chunk| match chunk {
134            Ok(bytes) => {
135                received = received.saturating_add(bytes.len());
136                Ok(bytes)
137            }
138            Err(source) => Err(Error::Body {
139                status,
140                headers: Box::new(headers.clone()),
141                received,
142                source: TransportError::from(source),
143            }),
144        })
145    }
146
147    /// Decodes the body into a type other than the endpoint's default.
148    pub async fn json_as<U: DeserializeOwned>(self) -> Result<U> {
149        let status = self.status_code();
150        let headers = Box::new(self.headers().clone());
151        #[cfg(feature = "tracing")]
152        let operation = self.operation;
153        let body = self.bytes().await?;
154        serde_json::from_slice(&body).map_err(|source| {
155            #[cfg(feature = "tracing")]
156            super::trace::decode_failed(operation, status, &source);
157            Error::Decode {
158                status,
159                headers,
160                body: String::from_utf8_lossy(
161                    &body[..body.len().min(super::transport::ERROR_LIMIT)],
162                )
163                .into_owned(),
164                source: DecodeError::new(source),
165            }
166        })
167    }
168}
169
170impl<T: DeserializeOwned> Response<T> {
171    /// Reads and decodes the body into the endpoint's response type.
172    /// Invalid JSON or a mismatched shape returns [`Error::Decode`] with response
173    /// status and headers. Reading remains subject to the transport's size limit.
174    pub async fn json(self) -> Result<T> {
175        self.json_as().await
176    }
177}
178
179/// Omits query values, which can carry filters or tokens.
180impl<T> fmt::Debug for Response<T> {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        f.debug_struct("Response")
183            .field("status", &self.status_code())
184            .field("url", &super::redacted(self.url()))
185            .finish_non_exhaustive()
186    }
187}
188
189/// At most `limit` bytes of a body, with how reading it ended.
190pub(crate) struct Bounded {
191    pub(crate) bytes: Vec<u8>,
192    /// The body was longer than `limit`.
193    pub(crate) truncated: bool,
194    /// Reading failed; `bytes` holds what arrived first.
195    pub(crate) error: Option<TransportError>,
196}
197
198pub(crate) async fn read_bounded(response: reqwest::Response, limit: usize) -> Bounded {
199    let mut stream = response.bytes_stream();
200    let mut bytes = Vec::new();
201    while let Some(chunk) = stream.next().await {
202        let chunk = match chunk {
203            Ok(chunk) => chunk,
204            Err(error) => {
205                return Bounded {
206                    bytes,
207                    truncated: false,
208                    error: Some(TransportError::from(error)),
209                };
210            }
211        };
212        let remaining = limit.saturating_sub(bytes.len());
213        bytes.extend_from_slice(&chunk[..chunk.len().min(remaining)]);
214        if chunk.len() > remaining {
215            return Bounded {
216                bytes,
217                truncated: true,
218                error: None,
219            };
220        }
221    }
222    Bounded {
223        bytes,
224        truncated: false,
225        error: None,
226    }
227}