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
198impl Response<SdkBody> {
199    /// Replaces this response's body with [`SdkBody::taken()`]
200    pub fn take_body(&mut self) -> SdkBody {
201        std::mem::replace(self.body_mut(), SdkBody::taken())
202    }
203}
204
205#[cfg(feature = "http-02x")]
206impl<B> TryFrom<http_02x::Response<B>> for Response<B> {
207    type Error = HttpError;
208
209    fn try_from(value: http_02x::Response<B>) -> Result<Self, Self::Error> {
210        let (parts, body) = value.into_parts();
211        let headers = Headers::try_from(parts.headers)?;
212        Ok(Self {
213            status: StatusCode::try_from(parts.status.as_u16()).expect("validated by http 0.x"),
214            body,
215            extensions: parts.extensions.into(),
216            headers,
217        })
218    }
219}
220
221#[cfg(feature = "http-1x")]
222impl<B> TryFrom<http_1x::Response<B>> for Response<B> {
223    type Error = HttpError;
224
225    fn try_from(value: http_1x::Response<B>) -> Result<Self, Self::Error> {
226        let (parts, body) = value.into_parts();
227        let headers = Headers::try_from(parts.headers)?;
228        Ok(Self {
229            status: StatusCode::try_from(parts.status.as_u16()).expect("validated by http 1.x"),
230            body,
231            extensions: parts.extensions.into(),
232            headers,
233        })
234    }
235}
236
237#[cfg(all(test, feature = "http-02x", feature = "http-1x"))]
238mod test {
239    use super::*;
240    use aws_smithy_types::body::SdkBody;
241
242    #[test]
243    fn non_ascii_responses() {
244        let response = http_02x::Response::builder()
245            .status(200)
246            .header("k", "😹")
247            .body(SdkBody::empty())
248            .unwrap();
249        let response: Response = response
250            .try_into()
251            .expect("failed to convert a non-string header");
252        assert_eq!(response.headers().get("k"), Some("😹"))
253    }
254
255    #[test]
256    fn response_can_be_created() {
257        let req = http_02x::Response::builder()
258            .status(200)
259            .body(SdkBody::from("hello"))
260            .unwrap();
261        let mut rsp = super::Response::try_from(req).unwrap();
262        rsp.headers_mut().insert("a", "b");
263        assert_eq!("b", rsp.headers().get("a").unwrap());
264        rsp.headers_mut().append("a", "c");
265        assert_eq!("b", rsp.headers().get("a").unwrap());
266        let http0 = rsp.try_into_http02x().unwrap();
267        assert_eq!(200, http0.status().as_u16());
268    }
269
270    macro_rules! resp_eq {
271        ($a: expr, $b: expr) => {{
272            assert_eq!($a.status(), $b.status(), "status code mismatch");
273            assert_eq!($a.headers(), $b.headers(), "header mismatch");
274            assert_eq!($a.body().bytes(), $b.body().bytes(), "data mismatch");
275            assert_eq!(
276                $a.extensions().len(),
277                $b.extensions().len(),
278                "extensions size mismatch"
279            );
280        }};
281    }
282
283    #[track_caller]
284    fn check_roundtrip(req: impl Fn() -> http_02x::Response<SdkBody>) {
285        let mut container = super::Response::try_from(req()).unwrap();
286        container.add_extension(5_u32);
287        let mut h1 = container
288            .try_into_http1x()
289            .expect("failed converting to http_1x");
290        assert_eq!(h1.extensions().get::<u32>(), Some(&5));
291        h1.extensions_mut().remove::<u32>();
292
293        let mut container = super::Response::try_from(h1).expect("failed converting from http1x");
294        container.add_extension(5_u32);
295        let mut h0 = container
296            .try_into_http02x()
297            .expect("failed converting back to http_02x");
298        assert_eq!(h0.extensions().get::<u32>(), Some(&5));
299        h0.extensions_mut().remove::<u32>();
300        resp_eq!(h0, req());
301    }
302
303    #[test]
304    fn valid_round_trips() {
305        let response = || {
306            http_02x::Response::builder()
307                .status(200)
308                .header("k", "v")
309                .header("multi", "v1")
310                .header("multi", "v2")
311                .body(SdkBody::from("12345"))
312                .unwrap()
313        };
314        check_roundtrip(response);
315    }
316
317    #[test]
318    #[should_panic]
319    fn header_panics() {
320        let res = http_02x::Response::builder()
321            .status(200)
322            .body(SdkBody::from("hello"))
323            .unwrap();
324        let mut res = Response::try_from(res).unwrap();
325        let _ = res
326            .headers_mut()
327            .try_insert("a\nb", "a\nb")
328            .expect_err("invalid header");
329        let _ = res.headers_mut().insert("a\nb", "a\nb");
330    }
331
332    #[test]
333    fn cant_cross_convert_with_extensions_h0_h1() {
334        let resp_h0 = || {
335            http_02x::Response::builder()
336                .status(200)
337                .extension(5_u32)
338                .body(SdkBody::from("hello"))
339                .unwrap()
340        };
341
342        let _ = Response::try_from(resp_h0())
343            .unwrap()
344            .try_into_http1x()
345            .expect_err("cant copy extension");
346
347        let _ = Response::try_from(resp_h0())
348            .unwrap()
349            .try_into_http02x()
350            .expect("allowed to cross-copy");
351    }
352
353    #[test]
354    fn cant_cross_convert_with_extensions_h1_h0() {
355        let resp_h1 = || {
356            http_1x::Response::builder()
357                .status(200)
358                .extension(5_u32)
359                .body(SdkBody::from("hello"))
360                .unwrap()
361        };
362
363        let _ = Response::try_from(resp_h1())
364            .unwrap()
365            .try_into_http02x()
366            .expect_err("cant copy extension");
367
368        let _ = Response::try_from(resp_h1())
369            .unwrap()
370            .try_into_http1x()
371            .expect("allowed to cross-copy");
372    }
373}