aws_smithy_runtime_api/http/
response.rs
1use crate::http::extensions::Extensions;
9use crate::http::{Headers, HttpError};
10use aws_smithy_types::body::SdkBody;
11use std::fmt;
12
13#[derive(Copy, Clone, Debug, Eq, PartialEq)]
15pub struct StatusCode(u16);
16
17impl StatusCode {
18 pub fn is_success(self) -> bool {
20 (200..300).contains(&self.0)
21 }
22
23 pub fn is_client_error(self) -> bool {
25 (400..500).contains(&self.0)
26 }
27
28 pub fn is_server_error(self) -> bool {
30 (500..600).contains(&self.0)
31 }
32
33 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#[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 #[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 #[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 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 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 pub fn status(&self) -> StatusCode {
159 self.status
160 }
161
162 pub fn status_mut(&mut self) -> &mut StatusCode {
164 &mut self.status
165 }
166
167 pub fn headers(&self) -> &Headers {
169 &self.headers
170 }
171
172 pub fn headers_mut(&mut self) -> &mut Headers {
174 &mut self.headers
175 }
176
177 pub fn body(&self) -> &B {
179 &self.body
180 }
181
182 pub fn body_mut(&mut self) -> &mut B {
184 &mut self.body
185 }
186
187 pub fn into_body(self) -> B {
189 self.body
190 }
191
192 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 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}