aws_smithy_runtime_api/http/
request.rs1use crate::http::extensions::Extensions;
9use crate::http::Headers;
10use crate::http::HttpError;
11use aws_smithy_types::body::SdkBody;
12use std::borrow::Cow;
13
14#[non_exhaustive]
16pub struct RequestParts<B = SdkBody> {
17 pub uri: Uri,
19 pub headers: Headers,
21 pub body: B,
23}
24
25#[derive(Debug)]
26pub struct Request<B = SdkBody> {
28 body: B,
29 uri: Uri,
30 method: http_1x::Method,
31 extensions: Extensions,
32 headers: Headers,
33}
34
35#[derive(Debug, Clone)]
37pub struct Uri {
38 as_string: String,
39 parsed: ParsedUri,
40}
41
42#[derive(Debug, Clone)]
43enum ParsedUri {
44 H0(http_02x::Uri),
45 H1(http_1x::Uri),
46}
47
48impl ParsedUri {
49 fn path_and_query(&self) -> &str {
50 match &self {
51 ParsedUri::H0(u) => u.path_and_query().map(|pq| pq.as_str()).unwrap_or(""),
52 ParsedUri::H1(u) => u.path_and_query().map(|pq| pq.as_str()).unwrap_or(""),
53 }
54 }
55
56 fn path(&self) -> &str {
57 match &self {
58 ParsedUri::H0(u) => u.path(),
59 ParsedUri::H1(u) => u.path(),
60 }
61 }
62
63 fn query(&self) -> Option<&str> {
64 match &self {
65 ParsedUri::H0(u) => u.query(),
66 ParsedUri::H1(u) => u.query(),
67 }
68 }
69}
70
71impl Uri {
72 pub fn set_endpoint(&mut self, endpoint: &str) -> Result<(), HttpError> {
79 let endpoint: http_02x::Uri = endpoint.parse().map_err(HttpError::invalid_uri)?;
80 let endpoint = endpoint.into_parts();
81 let authority = endpoint
82 .authority
83 .ok_or_else(HttpError::missing_authority)?;
84 let scheme = endpoint.scheme.ok_or_else(HttpError::missing_scheme)?;
85 let new_uri = http_02x::Uri::builder()
86 .authority(authority)
87 .scheme(scheme)
88 .path_and_query(merge_paths(endpoint.path_and_query, &self.parsed).as_ref())
89 .build()
90 .map_err(HttpError::invalid_uri_parts)?;
91 self.as_string = new_uri.to_string();
92 self.parsed = ParsedUri::H0(new_uri);
93 Ok(())
94 }
95
96 pub fn path(&self) -> &str {
98 self.parsed.path()
99 }
100
101 pub fn query(&self) -> Option<&str> {
103 self.parsed.query()
104 }
105
106 fn from_http0x_uri(uri: http_02x::Uri) -> Self {
107 Self {
108 as_string: uri.to_string(),
109 parsed: ParsedUri::H0(uri),
110 }
111 }
112
113 #[allow(dead_code)]
114 fn from_http1x_uri(uri: http_1x::Uri) -> Self {
115 Self {
116 as_string: uri.to_string(),
117 parsed: ParsedUri::H1(uri),
118 }
119 }
120
121 #[allow(dead_code)]
122 fn into_h0(self) -> http_02x::Uri {
123 match self.parsed {
124 ParsedUri::H0(uri) => uri,
125 ParsedUri::H1(_uri) => self.as_string.parse().unwrap(),
126 }
127 }
128}
129
130fn merge_paths(
131 endpoint_path: Option<http_02x::uri::PathAndQuery>,
132 uri: &ParsedUri,
133) -> Cow<'_, str> {
134 let uri_path_and_query = uri.path_and_query();
135 let endpoint_path = match endpoint_path {
136 None => return Cow::Borrowed(uri_path_and_query),
137 Some(path) => path,
138 };
139 if let Some(query) = endpoint_path.query() {
140 tracing::warn!(query = %query, "query specified in endpoint will be ignored during endpoint resolution");
141 }
142 let endpoint_path = endpoint_path.path();
143 if endpoint_path.is_empty() {
144 Cow::Borrowed(uri_path_and_query)
145 } else {
146 let ep_no_slash = endpoint_path.strip_suffix('/').unwrap_or(endpoint_path);
147 let uri_path_no_slash = uri_path_and_query
148 .strip_prefix('/')
149 .unwrap_or(uri_path_and_query);
150 Cow::Owned(format!("{ep_no_slash}/{uri_path_no_slash}"))
151 }
152}
153
154impl TryFrom<String> for Uri {
155 type Error = HttpError;
156
157 fn try_from(value: String) -> Result<Self, Self::Error> {
158 let parsed = ParsedUri::H0(value.parse().map_err(HttpError::invalid_uri)?);
159 Ok(Uri {
160 as_string: value,
161 parsed,
162 })
163 }
164}
165
166impl<'a> TryFrom<&'a str> for Uri {
167 type Error = HttpError;
168 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
169 Self::try_from(value.to_string())
170 }
171}
172
173#[cfg(feature = "http-02x")]
174impl From<http_02x::Uri> for Uri {
175 fn from(value: http_02x::Uri) -> Self {
176 Uri::from_http0x_uri(value)
177 }
178}
179
180#[cfg(feature = "http-02x")]
181impl<B> TryInto<http_02x::Request<B>> for Request<B> {
182 type Error = HttpError;
183
184 fn try_into(self) -> Result<http_02x::Request<B>, Self::Error> {
185 self.try_into_http02x()
186 }
187}
188
189#[cfg(feature = "http-1x")]
190impl From<http_1x::Uri> for Uri {
191 fn from(value: http_1x::Uri) -> Self {
192 Uri::from_http1x_uri(value)
193 }
194}
195
196#[cfg(feature = "http-1x")]
197impl<B> TryInto<http_1x::Request<B>> for Request<B> {
198 type Error = HttpError;
199
200 fn try_into(self) -> Result<http_1x::Request<B>, Self::Error> {
201 self.try_into_http1x()
202 }
203}
204
205impl<B> Request<B> {
206 #[cfg(feature = "http-02x")]
211 pub fn try_into_http02x(self) -> Result<http_02x::Request<B>, HttpError> {
212 let mut req = http_02x::Request::builder()
213 .uri(self.uri.into_h0())
214 .method(
215 http_02x::Method::from_bytes(self.method.as_str().as_bytes())
216 .expect("valid method"),
217 )
218 .body(self.body)
219 .expect("known valid");
220 *req.headers_mut() = self.headers.http0_headermap();
221 *req.extensions_mut() = self.extensions.try_into()?;
222 Ok(req)
223 }
224
225 #[cfg(feature = "http-1x")]
230 pub fn try_into_http1x(self) -> Result<http_1x::Request<B>, HttpError> {
231 let mut req = http_1x::Request::builder()
232 .uri(self.uri.as_string)
233 .method(self.method)
234 .body(self.body)
235 .expect("known valid");
236 *req.headers_mut() = self.headers.http1_headermap();
237 *req.extensions_mut() = self.extensions.try_into()?;
238 Ok(req)
239 }
240
241 pub fn map<U>(self, f: impl Fn(B) -> U) -> Request<U> {
243 Request {
244 body: f(self.body),
245 uri: self.uri,
246 method: self.method,
247 extensions: self.extensions,
248 headers: self.headers,
249 }
250 }
251
252 pub fn new(body: B) -> Self {
254 Self {
255 body,
256 uri: Uri::from_http0x_uri(http_02x::Uri::from_static("/")),
257 method: http_1x::Method::GET,
258 extensions: Default::default(),
259 headers: Default::default(),
260 }
261 }
262
263 pub fn into_parts(self) -> RequestParts<B> {
265 RequestParts {
266 uri: self.uri,
267 headers: self.headers,
268 body: self.body,
269 }
270 }
271
272 pub fn headers(&self) -> &Headers {
274 &self.headers
275 }
276
277 pub fn headers_mut(&mut self) -> &mut Headers {
279 &mut self.headers
280 }
281
282 pub fn body(&self) -> &B {
284 &self.body
285 }
286
287 pub fn body_mut(&mut self) -> &mut B {
289 &mut self.body
290 }
291
292 pub fn into_body(self) -> B {
294 self.body
295 }
296
297 pub fn method(&self) -> &str {
299 self.method.as_str()
300 }
301
302 pub fn set_method(&mut self, method: &str) -> Result<(), HttpError> {
304 self.method =
305 http_1x::Method::from_bytes(method.as_bytes()).map_err(HttpError::invalid_method)?;
306 Ok(())
307 }
308
309 pub fn uri(&self) -> &str {
311 &self.uri.as_string
312 }
313
314 pub fn uri_mut(&mut self) -> &mut Uri {
316 &mut self.uri
317 }
318
319 pub fn set_uri<U>(&mut self, uri: U) -> Result<(), U::Error>
321 where
322 U: TryInto<Uri>,
323 {
324 let uri = uri.try_into()?;
325 self.uri = uri;
326 Ok(())
327 }
328
329 pub fn add_extension<T: Send + Sync + Clone + 'static>(&mut self, extension: T) {
331 self.extensions.insert(extension.clone());
332 }
333}
334
335impl Request<SdkBody> {
336 pub fn try_clone(&self) -> Option<Self> {
342 let body = self.body().try_clone()?;
343 Some(Self {
344 body,
345 uri: self.uri.clone(),
346 method: self.method.clone(),
347 extensions: Extensions::new(),
348 headers: self.headers.clone(),
349 })
350 }
351
352 pub fn take_body(&mut self) -> SdkBody {
354 std::mem::replace(self.body_mut(), SdkBody::taken())
355 }
356
357 pub fn empty() -> Self {
359 Self::new(SdkBody::empty())
360 }
361
362 pub fn get(uri: impl AsRef<str>) -> Result<Self, HttpError> {
364 let mut req = Self::new(SdkBody::empty());
365 req.set_uri(uri.as_ref())?;
366 Ok(req)
367 }
368}
369
370#[cfg(feature = "http-02x")]
371impl<B> TryFrom<http_02x::Request<B>> for Request<B> {
372 type Error = HttpError;
373
374 fn try_from(value: http_02x::Request<B>) -> Result<Self, Self::Error> {
375 let (parts, body) = value.into_parts();
376 let headers = Headers::try_from(parts.headers)?;
377 Ok(Self {
378 body,
379 uri: parts.uri.into(),
380 method: http_1x::Method::from_bytes(parts.method.as_str().as_bytes())
381 .expect("valid method"),
382 extensions: parts.extensions.into(),
383 headers,
384 })
385 }
386}
387
388#[cfg(feature = "http-1x")]
389impl<B> TryFrom<http_1x::Request<B>> for Request<B> {
390 type Error = HttpError;
391
392 fn try_from(value: http_1x::Request<B>) -> Result<Self, Self::Error> {
393 let (parts, body) = value.into_parts();
394 let headers = Headers::try_from(parts.headers)?;
395 Ok(Self {
396 body,
397 uri: Uri::from_http1x_uri(parts.uri),
398 method: parts.method,
399 extensions: parts.extensions.into(),
400 headers,
401 })
402 }
403}
404
405#[cfg(all(test, feature = "http-02x", feature = "http-1x"))]
406mod test {
407 use aws_smithy_types::body::SdkBody;
408 use http_02x::header::{AUTHORIZATION, CONTENT_LENGTH};
409
410 #[test]
411 fn non_ascii_requests() {
412 let request = http_02x::Request::builder()
413 .header("k", "😹")
414 .body(SdkBody::empty())
415 .unwrap();
416 let request: super::Request = request
417 .try_into()
418 .expect("failed to convert a non-string header");
419 assert_eq!(request.headers().get("k"), Some("😹"))
420 }
421
422 #[test]
423 fn request_can_be_created() {
424 let req = http_02x::Request::builder()
425 .uri("http://foo.com")
426 .body(SdkBody::from("hello"))
427 .unwrap();
428 let mut req = super::Request::try_from(req).unwrap();
429 req.headers_mut().insert("a", "b");
430 assert_eq!(req.headers().get("a").unwrap(), "b");
431 req.headers_mut().append("a", "c");
432 assert_eq!(req.headers().get("a").unwrap(), "b");
433 let http0 = req.try_into_http02x().unwrap();
434 assert_eq!(http0.uri(), "http://foo.com");
435 }
436
437 #[test]
438 fn uri_mutations() {
439 let req = http_02x::Request::builder()
440 .uri("http://foo.com")
441 .body(SdkBody::from("hello"))
442 .unwrap();
443 let mut req = super::Request::try_from(req).unwrap();
444 assert_eq!(req.uri(), "http://foo.com/");
445 req.set_uri("http://bar.com").unwrap();
446 assert_eq!(req.uri(), "http://bar.com");
447 let http0 = req.try_into_http02x().unwrap();
448 assert_eq!(http0.uri(), "http://bar.com");
449 }
450
451 #[test]
452 #[should_panic]
453 fn header_panics() {
454 let req = http_02x::Request::builder()
455 .uri("http://foo.com")
456 .body(SdkBody::from("hello"))
457 .unwrap();
458 let mut req = super::Request::try_from(req).unwrap();
459 let _ = req
460 .headers_mut()
461 .try_insert("a\nb", "a\nb")
462 .expect_err("invalid header");
463 let _ = req.headers_mut().insert("a\nb", "a\nb");
464 }
465
466 #[test]
467 fn try_clone_clones_all_data() {
468 let request = http_02x::Request::builder()
469 .uri(http_02x::Uri::from_static("https://www.amazon.com"))
470 .method("POST")
471 .header(CONTENT_LENGTH, 456)
472 .header(AUTHORIZATION, "Token: hello")
473 .body(SdkBody::from("hello world!"))
474 .expect("valid request");
475
476 let request: super::Request = request.try_into().unwrap();
477 let cloned = request.try_clone().expect("request is cloneable");
478
479 assert_eq!("https://www.amazon.com/", cloned.uri());
480 assert_eq!("POST", cloned.method());
481 assert_eq!(2, cloned.headers().len());
482 assert_eq!("Token: hello", cloned.headers().get(AUTHORIZATION).unwrap(),);
483 assert_eq!("456", cloned.headers().get(CONTENT_LENGTH).unwrap());
484 assert_eq!("hello world!".as_bytes(), cloned.body().bytes().unwrap());
485 }
486
487 #[test]
488 fn valid_round_trips() {
489 let request = || {
490 http_02x::Request::builder()
491 .uri(http_02x::Uri::from_static("https://www.amazon.com"))
492 .method("POST")
493 .header(CONTENT_LENGTH, 456)
494 .header(AUTHORIZATION, "Token: hello")
495 .header("multi", "v1")
496 .header("multi", "v2")
497 .body(SdkBody::from("hello world!"))
498 .expect("valid request")
499 };
500
501 check_roundtrip(request);
502 }
503
504 macro_rules! req_eq {
505 ($a: expr, $b: expr) => {{
506 assert_eq!($a.uri(), $b.uri(), "status code mismatch");
507 assert_eq!($a.headers(), $b.headers(), "header mismatch");
508 assert_eq!($a.method(), $b.method(), "header mismatch");
509 assert_eq!($a.body().bytes(), $b.body().bytes(), "data mismatch");
510 assert_eq!(
511 $a.extensions().len(),
512 $b.extensions().len(),
513 "extensions size mismatch"
514 );
515 }};
516 }
517
518 #[track_caller]
519 fn check_roundtrip(req: impl Fn() -> http_02x::Request<SdkBody>) {
520 let mut container = super::Request::try_from(req()).unwrap();
521 container.add_extension(5_u32);
522 let mut h1 = container
523 .try_into_http1x()
524 .expect("failed converting to http1x");
525 assert_eq!(h1.extensions().get::<u32>(), Some(&5));
526 h1.extensions_mut().remove::<u32>();
527
528 let mut container = super::Request::try_from(h1).expect("failed converting from http1x");
529 container.add_extension(5_u32);
530 let mut h0 = container
531 .try_into_http02x()
532 .expect("failed converting back to http0x");
533 assert_eq!(h0.extensions().get::<u32>(), Some(&5));
534 h0.extensions_mut().remove::<u32>();
535 req_eq!(h0, req());
536 }
537}