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-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 assert_eq!(rsp.extension::<Marker>(), None);
283 rsp.add_extension(Marker(7));
284 assert_eq!(rsp.extension::<Marker>(), Some(&Marker(7)));
286 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}