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 #[cfg(feature = "http-02x")]
45 H0(http_02x::Uri),
46 H1(http_1x::Uri),
47}
48
49impl ParsedUri {
50 fn path_and_query(&self) -> &str {
51 match &self {
52 #[cfg(feature = "http-02x")]
53 ParsedUri::H0(u) => u.path_and_query().map(|pq| pq.as_str()).unwrap_or(""),
54 ParsedUri::H1(u) => u.path_and_query().map(|pq| pq.as_str()).unwrap_or(""),
55 }
56 }
57
58 fn path(&self) -> &str {
59 match &self {
60 #[cfg(feature = "http-02x")]
61 ParsedUri::H0(u) => u.path(),
62 ParsedUri::H1(u) => u.path(),
63 }
64 }
65
66 fn query(&self) -> Option<&str> {
67 match &self {
68 #[cfg(feature = "http-02x")]
69 ParsedUri::H0(u) => u.query(),
70 ParsedUri::H1(u) => u.query(),
71 }
72 }
73}
74
75impl Uri {
76 pub fn set_endpoint(&mut self, endpoint: &str) -> Result<(), HttpError> {
83 let endpoint: http_1x::Uri = endpoint.parse().map_err(HttpError::invalid_uri)?;
84 let endpoint = endpoint.into_parts();
85 let authority = endpoint
86 .authority
87 .ok_or_else(HttpError::missing_authority)?;
88 let scheme = endpoint.scheme.ok_or_else(HttpError::missing_scheme)?;
89 let new_uri = http_1x::Uri::builder()
90 .authority(authority)
91 .scheme(scheme)
92 .path_and_query(merge_paths(endpoint.path_and_query, &self.parsed).as_ref())
93 .build()
94 .map_err(HttpError::invalid_uri_parts)?;
95 self.as_string = new_uri.to_string();
96 self.parsed = ParsedUri::H1(new_uri);
97 Ok(())
98 }
99
100 pub fn path(&self) -> &str {
102 self.parsed.path()
103 }
104
105 pub fn query(&self) -> Option<&str> {
107 self.parsed.query()
108 }
109
110 #[cfg(feature = "http-02x")]
111 fn from_http0x_uri(uri: http_02x::Uri) -> Self {
112 Self {
113 as_string: uri.to_string(),
114 parsed: ParsedUri::H0(uri),
115 }
116 }
117
118 fn from_http1x_uri(uri: http_1x::Uri) -> Self {
119 Self {
120 as_string: uri.to_string(),
121 parsed: ParsedUri::H1(uri),
122 }
123 }
124
125 #[cfg(feature = "http-02x")]
126 fn into_h0(self) -> Result<http_02x::Uri, HttpError> {
127 match self.parsed {
128 ParsedUri::H0(uri) => Ok(uri),
129 ParsedUri::H1(_uri) => self.as_string.parse().map_err(HttpError::invalid_uri_h0),
132 }
133 }
134
135 #[cfg(feature = "http-1x")]
136 fn into_h1(self) -> http_1x::Uri {
137 match self.parsed {
138 ParsedUri::H1(uri) => uri,
140 #[cfg(feature = "http-02x")]
141 ParsedUri::H0(_uri) => self
142 .as_string
143 .parse()
144 .expect("an http 0.2.x uri is a valid http 1.x uri"),
145 }
146 }
147}
148
149fn merge_paths(endpoint_path: Option<http_1x::uri::PathAndQuery>, uri: &ParsedUri) -> Cow<'_, str> {
150 let uri_path_and_query = uri.path_and_query();
151 let endpoint_path = match endpoint_path {
152 None => return Cow::Borrowed(uri_path_and_query),
153 Some(path) => path,
154 };
155 if let Some(query) = endpoint_path.query() {
156 tracing::warn!(query = %query, "query specified in endpoint will be ignored during endpoint resolution");
157 }
158 let endpoint_path = endpoint_path.path();
159 if endpoint_path.is_empty() {
160 Cow::Borrowed(uri_path_and_query)
161 } else {
162 let ep_no_slash = endpoint_path.strip_suffix('/').unwrap_or(endpoint_path);
163 let uri_path_no_slash = uri_path_and_query
164 .strip_prefix('/')
165 .unwrap_or(uri_path_and_query);
166 Cow::Owned(format!("{ep_no_slash}/{uri_path_no_slash}"))
167 }
168}
169
170impl TryFrom<String> for Uri {
171 type Error = HttpError;
172
173 fn try_from(value: String) -> Result<Self, Self::Error> {
174 let parsed = ParsedUri::H1(value.parse().map_err(HttpError::invalid_uri)?);
175 Ok(Uri {
176 as_string: value,
177 parsed,
178 })
179 }
180}
181
182impl<'a> TryFrom<&'a str> for Uri {
183 type Error = HttpError;
184 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
185 Self::try_from(value.to_string())
186 }
187}
188
189#[cfg(feature = "http-02x")]
190impl From<http_02x::Uri> for Uri {
191 fn from(value: http_02x::Uri) -> Self {
192 Uri::from_http0x_uri(value)
193 }
194}
195
196#[cfg(feature = "http-02x")]
197impl<B> TryInto<http_02x::Request<B>> for Request<B> {
198 type Error = HttpError;
199
200 fn try_into(self) -> Result<http_02x::Request<B>, Self::Error> {
201 self.try_into_http02x()
202 }
203}
204
205#[cfg(feature = "http-1x")]
206impl From<http_1x::Uri> for Uri {
207 fn from(value: http_1x::Uri) -> Self {
208 Uri::from_http1x_uri(value)
209 }
210}
211
212#[cfg(feature = "http-1x")]
213impl<B> TryInto<http_1x::Request<B>> for Request<B> {
214 type Error = HttpError;
215
216 fn try_into(self) -> Result<http_1x::Request<B>, Self::Error> {
217 self.try_into_http1x()
218 }
219}
220
221impl<B> Request<B> {
222 #[cfg(feature = "http-02x")]
227 pub fn try_into_http02x(self) -> Result<http_02x::Request<B>, HttpError> {
228 let mut req = http_02x::Request::builder()
229 .uri(self.uri.into_h0()?)
230 .method(
231 http_02x::Method::from_bytes(self.method.as_str().as_bytes())
232 .expect("valid method"),
233 )
234 .body(self.body)
235 .expect("known valid");
236 *req.headers_mut() = self.headers.http0_headermap();
237 *req.extensions_mut() = self.extensions.try_into()?;
238 Ok(req)
239 }
240
241 #[cfg(feature = "http-1x")]
246 pub fn try_into_http1x(self) -> Result<http_1x::Request<B>, HttpError> {
247 let mut req = http_1x::Request::builder()
248 .uri(self.uri.into_h1())
249 .method(self.method)
250 .body(self.body)
251 .expect("known valid");
252 *req.headers_mut() = self.headers.http1_headermap();
253 *req.extensions_mut() = self.extensions.try_into()?;
254 Ok(req)
255 }
256
257 pub fn map<U>(self, f: impl Fn(B) -> U) -> Request<U> {
259 Request {
260 body: f(self.body),
261 uri: self.uri,
262 method: self.method,
263 extensions: self.extensions,
264 headers: self.headers,
265 }
266 }
267
268 pub fn new(body: B) -> Self {
270 Self {
271 body,
272 uri: Uri::from_http1x_uri(http_1x::Uri::from_static("/")),
273 method: http_1x::Method::GET,
274 extensions: Default::default(),
275 headers: Default::default(),
276 }
277 }
278
279 pub fn into_parts(self) -> RequestParts<B> {
281 RequestParts {
282 uri: self.uri,
283 headers: self.headers,
284 body: self.body,
285 }
286 }
287
288 pub fn headers(&self) -> &Headers {
290 &self.headers
291 }
292
293 pub fn headers_mut(&mut self) -> &mut Headers {
295 &mut self.headers
296 }
297
298 pub fn body(&self) -> &B {
300 &self.body
301 }
302
303 pub fn body_mut(&mut self) -> &mut B {
305 &mut self.body
306 }
307
308 pub fn into_body(self) -> B {
310 self.body
311 }
312
313 pub fn method(&self) -> &str {
315 self.method.as_str()
316 }
317
318 pub fn set_method(&mut self, method: &str) -> Result<(), HttpError> {
320 self.method =
321 http_1x::Method::from_bytes(method.as_bytes()).map_err(HttpError::invalid_method)?;
322 Ok(())
323 }
324
325 pub fn uri(&self) -> &str {
327 &self.uri.as_string
328 }
329
330 pub fn uri_mut(&mut self) -> &mut Uri {
332 &mut self.uri
333 }
334
335 pub fn set_uri<U>(&mut self, uri: U) -> Result<(), U::Error>
337 where
338 U: TryInto<Uri>,
339 {
340 let uri = uri.try_into()?;
341 self.uri = uri;
342 Ok(())
343 }
344
345 pub fn add_extension<T: Send + Sync + Clone + 'static>(&mut self, extension: T) {
347 self.extensions.insert(extension.clone());
348 }
349}
350
351impl Request<SdkBody> {
352 pub fn try_clone(&self) -> Option<Self> {
358 let body = self.body().try_clone()?;
359 Some(Self {
360 body,
361 uri: self.uri.clone(),
362 method: self.method.clone(),
363 extensions: Extensions::new(),
364 headers: self.headers.clone(),
365 })
366 }
367
368 pub fn take_body(&mut self) -> SdkBody {
370 std::mem::replace(self.body_mut(), SdkBody::taken())
371 }
372
373 pub fn empty() -> Self {
375 Self::new(SdkBody::empty())
376 }
377
378 pub fn get(uri: impl AsRef<str>) -> Result<Self, HttpError> {
380 let mut req = Self::new(SdkBody::empty());
381 req.set_uri(uri.as_ref())?;
382 Ok(req)
383 }
384}
385
386#[cfg(feature = "http-02x")]
387impl<B> TryFrom<http_02x::Request<B>> for Request<B> {
388 type Error = HttpError;
389
390 fn try_from(value: http_02x::Request<B>) -> Result<Self, Self::Error> {
391 let (parts, body) = value.into_parts();
392 let headers = Headers::try_from(parts.headers)?;
393 Ok(Self {
394 body,
395 uri: parts.uri.into(),
396 method: http_1x::Method::from_bytes(parts.method.as_str().as_bytes())
397 .expect("valid method"),
398 extensions: parts.extensions.into(),
399 headers,
400 })
401 }
402}
403
404#[cfg(feature = "http-1x")]
405impl<B> TryFrom<http_1x::Request<B>> for Request<B> {
406 type Error = HttpError;
407
408 fn try_from(value: http_1x::Request<B>) -> Result<Self, Self::Error> {
409 let (parts, body) = value.into_parts();
410 let headers = Headers::try_from(parts.headers)?;
411 Ok(Self {
412 body,
413 uri: Uri::from_http1x_uri(parts.uri),
414 method: parts.method,
415 extensions: parts.extensions.into(),
416 headers,
417 })
418 }
419}
420
421#[cfg(all(test, feature = "http-1x"))]
422mod test {
423 use aws_smithy_types::body::SdkBody;
424 use http_1x::header::{AUTHORIZATION, CONTENT_LENGTH};
425
426 #[test]
427 fn non_ascii_requests() {
428 let request = http_1x::Request::builder()
429 .header("k", "😹")
430 .body(SdkBody::empty())
431 .unwrap();
432 let request: super::Request = request
433 .try_into()
434 .expect("failed to convert a non-string header");
435 assert_eq!(request.headers().get("k"), Some("😹"))
436 }
437
438 #[test]
439 fn request_can_be_created() {
440 let req = http_1x::Request::builder()
441 .uri("http://foo.com")
442 .body(SdkBody::from("hello"))
443 .unwrap();
444 let mut req = super::Request::try_from(req).unwrap();
445 req.headers_mut().insert("a", "b");
446 assert_eq!(req.headers().get("a").unwrap(), "b");
447 req.headers_mut().append("a", "c");
448 assert_eq!(req.headers().get("a").unwrap(), "b");
449 let http1 = req.try_into_http1x().unwrap();
450 assert_eq!(http1.uri(), "http://foo.com");
451 }
452
453 #[test]
454 fn uri_mutations() {
455 let req = http_1x::Request::builder()
456 .uri("http://foo.com")
457 .body(SdkBody::from("hello"))
458 .unwrap();
459 let mut req = super::Request::try_from(req).unwrap();
460 assert_eq!(req.uri(), "http://foo.com/");
461 req.set_uri("http://bar.com").unwrap();
462 assert_eq!(req.uri(), "http://bar.com");
463 let http1 = req.try_into_http1x().unwrap();
464 assert_eq!(http1.uri(), "http://bar.com");
465 }
466
467 #[test]
468 fn set_endpoint_merges_paths() {
469 let mut req = super::Request::empty();
470 req.set_uri("/foo/bar").unwrap();
471 req.uri_mut()
472 .set_endpoint("https://www.amazon.com")
473 .unwrap();
474 assert_eq!(req.uri(), "https://www.amazon.com/foo/bar");
475 }
476
477 #[test]
478 #[should_panic]
479 fn header_panics() {
480 let req = http_1x::Request::builder()
481 .uri("http://foo.com")
482 .body(SdkBody::from("hello"))
483 .unwrap();
484 let mut req = super::Request::try_from(req).unwrap();
485 let _ = req
486 .headers_mut()
487 .try_insert("a\nb", "a\nb")
488 .expect_err("invalid header");
489 let _ = req.headers_mut().insert("a\nb", "a\nb");
490 }
491
492 #[test]
493 fn try_clone_clones_all_data() {
494 let request = http_1x::Request::builder()
495 .uri(http_1x::Uri::from_static("https://www.amazon.com"))
496 .method("POST")
497 .header(CONTENT_LENGTH, 456)
498 .header(AUTHORIZATION, "Token: hello")
499 .body(SdkBody::from("hello world!"))
500 .expect("valid request");
501
502 let request: super::Request = request.try_into().unwrap();
503 let cloned = request.try_clone().expect("request is cloneable");
504
505 assert_eq!("https://www.amazon.com/", cloned.uri());
506 assert_eq!("POST", cloned.method());
507 assert_eq!(2, cloned.headers().len());
508 assert_eq!("Token: hello", cloned.headers().get(AUTHORIZATION).unwrap(),);
509 assert_eq!("456", cloned.headers().get(CONTENT_LENGTH).unwrap());
510 assert_eq!("hello world!".as_bytes(), cloned.body().bytes().unwrap());
511 }
512}
513
514#[cfg(all(test, feature = "http-02x", feature = "http-1x"))]
515mod cross_version_test {
516 use super::Request;
517 use aws_smithy_types::body::SdkBody;
518 use http_02x::header::{AUTHORIZATION, CONTENT_LENGTH};
519
520 #[test]
523 fn converting_a_non_ascii_uri_to_http02x_does_not_panic() {
524 let mut req = Request::empty();
525 if req.set_uri("http://foo.com/\u{80}").is_err() {
526 return;
528 }
529 let result =
530 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| req.try_into_http02x()));
531 assert!(
532 result.is_ok(),
533 "Uri::into_h0's `self.as_string.parse()` panicked instead of surfacing an HttpError"
534 );
535 }
536
537 #[test]
538 fn valid_round_trips() {
539 let request = || {
540 http_02x::Request::builder()
541 .uri(http_02x::Uri::from_static("https://www.amazon.com"))
542 .method("POST")
543 .header(CONTENT_LENGTH, 456)
544 .header(AUTHORIZATION, "Token: hello")
545 .header("multi", "v1")
546 .header("multi", "v2")
547 .body(SdkBody::from("hello world!"))
548 .expect("valid request")
549 };
550
551 check_roundtrip(request);
552 }
553
554 macro_rules! req_eq {
555 ($a: expr, $b: expr) => {{
556 assert_eq!($a.uri(), $b.uri(), "status code mismatch");
557 assert_eq!($a.headers(), $b.headers(), "header mismatch");
558 assert_eq!($a.method(), $b.method(), "header mismatch");
559 assert_eq!($a.body().bytes(), $b.body().bytes(), "data mismatch");
560 assert_eq!(
561 $a.extensions().len(),
562 $b.extensions().len(),
563 "extensions size mismatch"
564 );
565 }};
566 }
567
568 #[track_caller]
569 fn check_roundtrip(req: impl Fn() -> http_02x::Request<SdkBody>) {
570 let mut container = super::Request::try_from(req()).unwrap();
571 container.add_extension(5_u32);
572 let mut h1 = container
573 .try_into_http1x()
574 .expect("failed converting to http1x");
575 assert_eq!(h1.extensions().get::<u32>(), Some(&5));
576 h1.extensions_mut().remove::<u32>();
577
578 let mut container = super::Request::try_from(h1).expect("failed converting from http1x");
579 container.add_extension(5_u32);
580 let mut h0 = container
581 .try_into_http02x()
582 .expect("failed converting back to http0x");
583 assert_eq!(h0.extensions().get::<u32>(), Some(&5));
584 h0.extensions_mut().remove::<u32>();
585 req_eq!(h0, req());
586 }
587}