aws_smithy_runtime_api/http/
response.rs1use 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 pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
199 self.extensions.get::<T>()
200 }
201}
202
203impl Response<SdkBody> {
204 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 #[test]
263 fn non_utf8_header_values_round_trip_byte_for_byte() {
264 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 assert_eq!(rsp.extension::<Marker>(), None);
325 rsp.add_extension(Marker(7));
326 assert_eq!(rsp.extension::<Marker>(), Some(&Marker(7)));
328 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}