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-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_1x::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    // A response carrying a header value that is not valid UTF-8 must convert successfully, and
261    // converting back out must reproduce the original octets rather than a re-encoded string.
262    #[test]
263    fn non_utf8_header_values_round_trip_byte_for_byte() {
264        // A lone 0xE9 is a valid HTTP header octet (obs-text per RFC 7230) but not valid UTF-8.
265        const NON_UTF8_VALUE: &[u8] = b"value-\xe9";
266
267        let response = http_1x::Response::builder()
268            .status(200)
269            .header(
270                "k",
271                http_1x::HeaderValue::from_bytes(NON_UTF8_VALUE).expect("valid header octets"),
272            )
273            .body(SdkBody::empty())
274            .unwrap();
275
276        let response: Response = response.try_into().expect("non-UTF-8 values are admitted");
277        assert_eq!(Some(NON_UTF8_VALUE), response.headers().get_bytes("k"));
278        assert_eq!(None, response.headers().get("k"));
279
280        let round_tripped = response.try_into_http1x().expect("converts back");
281        assert_eq!(
282            NON_UTF8_VALUE,
283            round_tripped.headers().get("k").unwrap().as_bytes()
284        );
285    }
286
287    #[test]
288    fn response_can_be_created() {
289        let req = http_1x::Response::builder()
290            .status(200)
291            .body(SdkBody::from("hello"))
292            .unwrap();
293        let mut rsp = super::Response::try_from(req).unwrap();
294        rsp.headers_mut().insert("a", "b");
295        assert_eq!("b", rsp.headers().get("a").unwrap());
296        rsp.headers_mut().append("a", "c");
297        assert_eq!("b", rsp.headers().get("a").unwrap());
298        let http1 = rsp.try_into_http1x().unwrap();
299        assert_eq!(200, http1.status().as_u16());
300    }
301
302    #[test]
303    #[should_panic]
304    fn header_panics() {
305        let res = http_1x::Response::builder()
306            .status(200)
307            .body(SdkBody::from("hello"))
308            .unwrap();
309        let mut res = Response::try_from(res).unwrap();
310        let _ = res
311            .headers_mut()
312            .try_insert("a\nb", "a\nb")
313            .expect_err("invalid header");
314        let _ = res.headers_mut().insert("a\nb", "a\nb");
315    }
316
317    #[test]
318    fn add_and_get_extension() {
319        #[derive(Clone, Debug, PartialEq)]
320        struct Marker(u32);
321
322        let mut rsp = super::Response::new(StatusCode::try_from(200).unwrap(), SdkBody::empty());
323        // Absent before insertion.
324        assert_eq!(rsp.extension::<Marker>(), None);
325        rsp.add_extension(Marker(7));
326        // Round-trips the value.
327        assert_eq!(rsp.extension::<Marker>(), Some(&Marker(7)));
328        // A type that was never inserted returns None.
329        assert_eq!(rsp.extension::<u64>(), None);
330    }
331}
332
333#[cfg(all(test, feature = "http-02x", feature = "http-1x"))]
334mod cross_version_test {
335    use super::*;
336    use aws_smithy_types::body::SdkBody;
337
338    macro_rules! resp_eq {
339        ($a: expr, $b: expr) => {{
340            assert_eq!($a.status(), $b.status(), "status code mismatch");
341            assert_eq!($a.headers(), $b.headers(), "header mismatch");
342            assert_eq!($a.body().bytes(), $b.body().bytes(), "data mismatch");
343            assert_eq!(
344                $a.extensions().len(),
345                $b.extensions().len(),
346                "extensions size mismatch"
347            );
348        }};
349    }
350
351    #[track_caller]
352    fn check_roundtrip(req: impl Fn() -> http_02x::Response<SdkBody>) {
353        let mut container = super::Response::try_from(req()).unwrap();
354        container.add_extension(5_u32);
355        let mut h1 = container
356            .try_into_http1x()
357            .expect("failed converting to http_1x");
358        assert_eq!(h1.extensions().get::<u32>(), Some(&5));
359        h1.extensions_mut().remove::<u32>();
360
361        let mut container = super::Response::try_from(h1).expect("failed converting from http1x");
362        container.add_extension(5_u32);
363        let mut h0 = container
364            .try_into_http02x()
365            .expect("failed converting back to http_02x");
366        assert_eq!(h0.extensions().get::<u32>(), Some(&5));
367        h0.extensions_mut().remove::<u32>();
368        resp_eq!(h0, req());
369    }
370
371    #[test]
372    fn valid_round_trips() {
373        let response = || {
374            http_02x::Response::builder()
375                .status(200)
376                .header("k", "v")
377                .header("multi", "v1")
378                .header("multi", "v2")
379                .body(SdkBody::from("12345"))
380                .unwrap()
381        };
382        check_roundtrip(response);
383    }
384
385    #[test]
386    fn cant_cross_convert_with_extensions_h0_h1() {
387        let resp_h0 = || {
388            http_02x::Response::builder()
389                .status(200)
390                .extension(5_u32)
391                .body(SdkBody::from("hello"))
392                .unwrap()
393        };
394
395        let _ = Response::try_from(resp_h0())
396            .unwrap()
397            .try_into_http1x()
398            .expect_err("cant copy extension");
399
400        let _ = Response::try_from(resp_h0())
401            .unwrap()
402            .try_into_http02x()
403            .expect("allowed to cross-copy");
404    }
405
406    #[test]
407    fn cant_cross_convert_with_extensions_h1_h0() {
408        let resp_h1 = || {
409            http_1x::Response::builder()
410                .status(200)
411                .extension(5_u32)
412                .body(SdkBody::from("hello"))
413                .unwrap()
414        };
415
416        let _ = Response::try_from(resp_h1())
417            .unwrap()
418            .try_into_http02x()
419            .expect_err("cant copy extension");
420
421        let _ = Response::try_from(resp_h1())
422            .unwrap()
423            .try_into_http1x()
424            .expect("allowed to cross-copy");
425    }
426}