Skip to main content

aws_smithy_runtime_api/http/
response.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Http Response Types
7
8use crate::http::extensions::Extensions;
9use crate::http::{Headers, HttpError};
10use aws_smithy_types::body::SdkBody;
11use std::fmt;
12
13/// HTTP response status code
14#[derive(Copy, Clone, Debug, Eq, PartialEq)]
15pub struct StatusCode(u16);
16
17impl StatusCode {
18    /// True if this is a successful response code (200, 201, etc)
19    pub fn is_success(self) -> bool {
20        (200..300).contains(&self.0)
21    }
22
23    /// True if this response code is a client error (4xx)
24    pub fn is_client_error(self) -> bool {
25        (400..500).contains(&self.0)
26    }
27
28    /// True if this response code is a server error (5xx)
29    pub fn is_server_error(self) -> bool {
30        (500..600).contains(&self.0)
31    }
32
33    /// Return the value of this status code as a `u16`.
34    pub fn as_u16(self) -> u16 {
35        self.0
36    }
37}
38
39impl TryFrom<u16> for StatusCode {
40    type Error = HttpError;
41
42    fn try_from(value: u16) -> Result<Self, Self::Error> {
43        if (100..1000).contains(&value) {
44            Ok(StatusCode(value))
45        } else {
46            Err(HttpError::invalid_status_code())
47        }
48    }
49}
50
51#[cfg(feature = "http-02x")]
52impl From<http_02x::StatusCode> for StatusCode {
53    fn from(value: http_02x::StatusCode) -> Self {
54        Self(value.as_u16())
55    }
56}
57
58#[cfg(feature = "http-02x")]
59impl From<StatusCode> for http_02x::StatusCode {
60    fn from(value: StatusCode) -> Self {
61        Self::from_u16(value.0).unwrap()
62    }
63}
64
65#[cfg(feature = "http-1x")]
66impl From<http_1x::StatusCode> for StatusCode {
67    fn from(value: http_1x::StatusCode) -> Self {
68        Self(value.as_u16())
69    }
70}
71
72#[cfg(feature = "http-1x")]
73impl From<StatusCode> for http_1x::StatusCode {
74    fn from(value: StatusCode) -> Self {
75        Self::from_u16(value.0).unwrap()
76    }
77}
78
79impl From<StatusCode> for u16 {
80    fn from(value: StatusCode) -> Self {
81        value.0
82    }
83}
84
85impl fmt::Display for StatusCode {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        self.0.fmt(f)
88    }
89}
90
91/// An HTTP Response Type
92#[derive(Debug)]
93pub struct Response<B = SdkBody> {
94    status: StatusCode,
95    headers: Headers,
96    body: B,
97    extensions: Extensions,
98}
99
100impl<B> Response<B> {
101    /// Converts this response into an http 0.x response.
102    ///
103    /// Depending on the internal storage type, this operation may be free or it may have an internal
104    /// cost.
105    #[cfg(feature = "http-02x")]
106    pub fn try_into_http02x(self) -> Result<http_02x::Response<B>, HttpError> {
107        let mut res = http_02x::Response::builder()
108            .status(
109                http_02x::StatusCode::from_u16(self.status.into())
110                    .expect("validated upon construction"),
111            )
112            .body(self.body)
113            .expect("known valid");
114        *res.headers_mut() = self.headers.http0_headermap();
115        *res.extensions_mut() = self.extensions.try_into()?;
116        Ok(res)
117    }
118
119    /// Converts this response into an http 1.x response.
120    ///
121    /// Depending on the internal storage type, this operation may be free or it may have an internal
122    /// cost.
123    #[cfg(feature = "http-1x")]
124    pub fn try_into_http1x(self) -> Result<http_1x::Response<B>, HttpError> {
125        let mut res = http_1x::Response::builder()
126            .status(
127                http_1x::StatusCode::from_u16(self.status.into())
128                    .expect("validated upon construction"),
129            )
130            .body(self.body)
131            .expect("known valid");
132        *res.headers_mut() = self.headers.http1_headermap();
133        *res.extensions_mut() = self.extensions.try_into()?;
134        Ok(res)
135    }
136
137    /// Update the body of this response to be a new body.
138    pub fn map<U>(self, f: impl Fn(B) -> U) -> Response<U> {
139        Response {
140            status: self.status,
141            body: f(self.body),
142            extensions: self.extensions,
143            headers: self.headers,
144        }
145    }
146
147    /// Returns a response with the given status and body
148    pub fn new(status: StatusCode, body: B) -> Self {
149        Self {
150            status,
151            body,
152            extensions: Default::default(),
153            headers: Default::default(),
154        }
155    }
156
157    /// Returns the status code
158    pub fn status(&self) -> StatusCode {
159        self.status
160    }
161
162    /// Returns a mutable reference to the status code
163    pub fn status_mut(&mut self) -> &mut StatusCode {
164        &mut self.status
165    }
166
167    /// Returns a reference to the header map
168    pub fn headers(&self) -> &Headers {
169        &self.headers
170    }
171
172    /// Returns a mutable reference to the header map
173    pub fn headers_mut(&mut self) -> &mut Headers {
174        &mut self.headers
175    }
176
177    /// Returns the body associated with the request
178    pub fn body(&self) -> &B {
179        &self.body
180    }
181
182    /// Returns a mutable reference to the body
183    pub fn body_mut(&mut self) -> &mut B {
184        &mut self.body
185    }
186
187    /// Converts this response into the response body.
188    pub fn into_body(self) -> B {
189        self.body
190    }
191
192    /// Adds an extension to the response extensions
193    pub fn add_extension<T: Send + Sync + Clone + 'static>(&mut self, extension: T) {
194        self.extensions.insert(extension);
195    }
196
197    /// Returns a reference to a previously [attached](Self::add_extension) extension of type `T`, if present.
198    pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
199        self.extensions.get::<T>()
200    }
201}
202
203impl Response<SdkBody> {
204    /// Replaces this response's body with [`SdkBody::taken()`]
205    pub fn take_body(&mut self) -> SdkBody {
206        std::mem::replace(self.body_mut(), SdkBody::taken())
207    }
208}
209
210#[cfg(feature = "http-02x")]
211impl<B> TryFrom<http_02x::Response<B>> for Response<B> {
212    type Error = HttpError;
213
214    fn try_from(value: http_02x::Response<B>) -> Result<Self, Self::Error> {
215        let (parts, body) = value.into_parts();
216        let headers = Headers::try_from(parts.headers)?;
217        Ok(Self {
218            status: StatusCode::try_from(parts.status.as_u16()).expect("validated by http 0.x"),
219            body,
220            extensions: parts.extensions.into(),
221            headers,
222        })
223    }
224}
225
226#[cfg(feature = "http-1x")]
227impl<B> TryFrom<http_1x::Response<B>> for Response<B> {
228    type Error = HttpError;
229
230    fn try_from(value: http_1x::Response<B>) -> Result<Self, Self::Error> {
231        let (parts, body) = value.into_parts();
232        let headers = Headers::try_from(parts.headers)?;
233        Ok(Self {
234            status: StatusCode::try_from(parts.status.as_u16()).expect("validated by http 1.x"),
235            body,
236            extensions: parts.extensions.into(),
237            headers,
238        })
239    }
240}
241
242#[cfg(all(test, feature = "http-02x", feature = "http-1x"))]
243mod test {
244    use super::*;
245    use aws_smithy_types::body::SdkBody;
246
247    #[test]
248    fn non_ascii_responses() {
249        let response = http_02x::Response::builder()
250            .status(200)
251            .header("k", "😹")
252            .body(SdkBody::empty())
253            .unwrap();
254        let response: Response = response
255            .try_into()
256            .expect("failed to convert a non-string header");
257        assert_eq!(response.headers().get("k"), Some("😹"))
258    }
259
260    #[test]
261    fn response_can_be_created() {
262        let req = http_02x::Response::builder()
263            .status(200)
264            .body(SdkBody::from("hello"))
265            .unwrap();
266        let mut rsp = super::Response::try_from(req).unwrap();
267        rsp.headers_mut().insert("a", "b");
268        assert_eq!("b", rsp.headers().get("a").unwrap());
269        rsp.headers_mut().append("a", "c");
270        assert_eq!("b", rsp.headers().get("a").unwrap());
271        let http0 = rsp.try_into_http02x().unwrap();
272        assert_eq!(200, http0.status().as_u16());
273    }
274
275    #[test]
276    fn add_and_get_extension() {
277        #[derive(Clone, Debug, PartialEq)]
278        struct Marker(u32);
279
280        let mut rsp = super::Response::new(StatusCode::try_from(200).unwrap(), SdkBody::empty());
281        // Absent before insertion.
282        assert_eq!(rsp.extension::<Marker>(), None);
283        rsp.add_extension(Marker(7));
284        // Round-trips the value.
285        assert_eq!(rsp.extension::<Marker>(), Some(&Marker(7)));
286        // A type that was never inserted returns None.
287        assert_eq!(rsp.extension::<u64>(), None);
288    }
289
290    macro_rules! resp_eq {
291        ($a: expr, $b: expr) => {{
292            assert_eq!($a.status(), $b.status(), "status code mismatch");
293            assert_eq!($a.headers(), $b.headers(), "header mismatch");
294            assert_eq!($a.body().bytes(), $b.body().bytes(), "data mismatch");
295            assert_eq!(
296                $a.extensions().len(),
297                $b.extensions().len(),
298                "extensions size mismatch"
299            );
300        }};
301    }
302
303    #[track_caller]
304    fn check_roundtrip(req: impl Fn() -> http_02x::Response<SdkBody>) {
305        let mut container = super::Response::try_from(req()).unwrap();
306        container.add_extension(5_u32);
307        let mut h1 = container
308            .try_into_http1x()
309            .expect("failed converting to http_1x");
310        assert_eq!(h1.extensions().get::<u32>(), Some(&5));
311        h1.extensions_mut().remove::<u32>();
312
313        let mut container = super::Response::try_from(h1).expect("failed converting from http1x");
314        container.add_extension(5_u32);
315        let mut h0 = container
316            .try_into_http02x()
317            .expect("failed converting back to http_02x");
318        assert_eq!(h0.extensions().get::<u32>(), Some(&5));
319        h0.extensions_mut().remove::<u32>();
320        resp_eq!(h0, req());
321    }
322
323    #[test]
324    fn valid_round_trips() {
325        let response = || {
326            http_02x::Response::builder()
327                .status(200)
328                .header("k", "v")
329                .header("multi", "v1")
330                .header("multi", "v2")
331                .body(SdkBody::from("12345"))
332                .unwrap()
333        };
334        check_roundtrip(response);
335    }
336
337    #[test]
338    #[should_panic]
339    fn header_panics() {
340        let res = http_02x::Response::builder()
341            .status(200)
342            .body(SdkBody::from("hello"))
343            .unwrap();
344        let mut res = Response::try_from(res).unwrap();
345        let _ = res
346            .headers_mut()
347            .try_insert("a\nb", "a\nb")
348            .expect_err("invalid header");
349        let _ = res.headers_mut().insert("a\nb", "a\nb");
350    }
351
352    #[test]
353    fn cant_cross_convert_with_extensions_h0_h1() {
354        let resp_h0 = || {
355            http_02x::Response::builder()
356                .status(200)
357                .extension(5_u32)
358                .body(SdkBody::from("hello"))
359                .unwrap()
360        };
361
362        let _ = Response::try_from(resp_h0())
363            .unwrap()
364            .try_into_http1x()
365            .expect_err("cant copy extension");
366
367        let _ = Response::try_from(resp_h0())
368            .unwrap()
369            .try_into_http02x()
370            .expect("allowed to cross-copy");
371    }
372
373    #[test]
374    fn cant_cross_convert_with_extensions_h1_h0() {
375        let resp_h1 = || {
376            http_1x::Response::builder()
377                .status(200)
378                .extension(5_u32)
379                .body(SdkBody::from("hello"))
380                .unwrap()
381        };
382
383        let _ = Response::try_from(resp_h1())
384            .unwrap()
385            .try_into_http02x()
386            .expect_err("cant copy extension");
387
388        let _ = Response::try_from(resp_h1())
389            .unwrap()
390            .try_into_http1x()
391            .expect("allowed to cross-copy");
392    }
393}