Skip to main content

aws_smithy_runtime_api/http/
request.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Http Request Types
7
8use crate::http::extensions::Extensions;
9use crate::http::Headers;
10use crate::http::HttpError;
11use aws_smithy_types::body::SdkBody;
12use std::borrow::Cow;
13
14/// Parts struct useful for structural decomposition that the [`Request`] type can be converted into.
15#[non_exhaustive]
16pub struct RequestParts<B = SdkBody> {
17    /// Request URI.
18    pub uri: Uri,
19    /// Request headers.
20    pub headers: Headers,
21    /// Request body.
22    pub body: B,
23}
24
25#[derive(Debug)]
26/// An HTTP Request Type
27pub struct Request<B = SdkBody> {
28    body: B,
29    uri: Uri,
30    method: http_1x::Method,
31    extensions: Extensions,
32    headers: Headers,
33}
34
35/// A Request URI
36#[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    /// Sets `endpoint` as the endpoint for a URL.
77    ///
78    /// An `endpoint` MUST contain a scheme and authority.
79    /// An `endpoint` MAY contain a port and path.
80    ///
81    /// An `endpoint` MUST NOT contain a query
82    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    /// Returns the URI path.
101    pub fn path(&self) -> &str {
102        self.parsed.path()
103    }
104
105    /// Returns the URI query string.
106    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            // The internal storage is now http 1.x, which accepts some URIs that http 0.2.x does
130            // not. Surface those as an error instead of panicking.
131            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            // The internal storage is http 1.x, so this is free (no re-parse).
139            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    /// Converts this request into an http 0.x request.
223    ///
224    /// Depending on the internal storage type, this operation may be free or it may have an internal
225    /// cost.
226    #[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    /// Converts this request into an http 1.x request.
242    ///
243    /// Depending on the internal storage type, this operation may be free or it may have an internal
244    /// cost.
245    #[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    /// Update the body of this request to be a new body.
258    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    /// Returns a GET request with no URI
269    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    /// Convert this request into its parts.
280    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    /// Returns a reference to the header map
289    pub fn headers(&self) -> &Headers {
290        &self.headers
291    }
292
293    /// Returns a mutable reference to the header map
294    pub fn headers_mut(&mut self) -> &mut Headers {
295        &mut self.headers
296    }
297
298    /// Returns the body associated with the request
299    pub fn body(&self) -> &B {
300        &self.body
301    }
302
303    /// Returns a mutable reference to the body
304    pub fn body_mut(&mut self) -> &mut B {
305        &mut self.body
306    }
307
308    /// Converts this request into the request body.
309    pub fn into_body(self) -> B {
310        self.body
311    }
312
313    /// Returns the method associated with this request
314    pub fn method(&self) -> &str {
315        self.method.as_str()
316    }
317
318    /// Sets the HTTP method for this request
319    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    /// Returns the URI associated with this request
326    pub fn uri(&self) -> &str {
327        &self.uri.as_string
328    }
329
330    /// Returns a mutable reference the the URI of this http::Request
331    pub fn uri_mut(&mut self) -> &mut Uri {
332        &mut self.uri
333    }
334
335    /// Sets the URI of this request
336    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    /// Adds an extension to the request extensions
346    pub fn add_extension<T: Send + Sync + Clone + 'static>(&mut self, extension: T) {
347        self.extensions.insert(extension.clone());
348    }
349
350    /// Returns an extension previously added to this request.
351    pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
352        self.extensions.get()
353    }
354}
355
356impl Request<SdkBody> {
357    /// Attempts to clone this request
358    ///
359    /// On clone, any extensions will be cleared.
360    ///
361    /// If the body is cloneable, this will clone the request. Otherwise `None` will be returned
362    pub fn try_clone(&self) -> Option<Self> {
363        let body = self.body().try_clone()?;
364        Some(Self {
365            body,
366            uri: self.uri.clone(),
367            method: self.method.clone(),
368            extensions: Extensions::new(),
369            headers: self.headers.clone(),
370        })
371    }
372
373    /// Replaces this request's body with [`SdkBody::taken()`]
374    pub fn take_body(&mut self) -> SdkBody {
375        std::mem::replace(self.body_mut(), SdkBody::taken())
376    }
377
378    /// Create a GET request to `/` with an empty body
379    pub fn empty() -> Self {
380        Self::new(SdkBody::empty())
381    }
382
383    /// Creates a GET request to `uri` with an empty body
384    pub fn get(uri: impl AsRef<str>) -> Result<Self, HttpError> {
385        let mut req = Self::new(SdkBody::empty());
386        req.set_uri(uri.as_ref())?;
387        Ok(req)
388    }
389}
390
391#[cfg(feature = "http-02x")]
392impl<B> TryFrom<http_02x::Request<B>> for Request<B> {
393    type Error = HttpError;
394
395    fn try_from(value: http_02x::Request<B>) -> Result<Self, Self::Error> {
396        let (parts, body) = value.into_parts();
397        let headers = Headers::try_from(parts.headers)?;
398        Ok(Self {
399            body,
400            uri: parts.uri.into(),
401            method: http_1x::Method::from_bytes(parts.method.as_str().as_bytes())
402                .expect("valid method"),
403            extensions: parts.extensions.into(),
404            headers,
405        })
406    }
407}
408
409#[cfg(feature = "http-1x")]
410impl<B> TryFrom<http_1x::Request<B>> for Request<B> {
411    type Error = HttpError;
412
413    fn try_from(value: http_1x::Request<B>) -> Result<Self, Self::Error> {
414        let (parts, body) = value.into_parts();
415        let headers = Headers::try_from(parts.headers)?;
416        Ok(Self {
417            body,
418            uri: Uri::from_http1x_uri(parts.uri),
419            method: parts.method,
420            extensions: parts.extensions.into(),
421            headers,
422        })
423    }
424}
425
426#[cfg(all(test, feature = "http-1x"))]
427mod test {
428    use aws_smithy_types::body::SdkBody;
429    use http_1x::header::{AUTHORIZATION, CONTENT_LENGTH};
430
431    #[test]
432    fn non_ascii_requests() {
433        let request = http_1x::Request::builder()
434            .header("k", "😹")
435            .body(SdkBody::empty())
436            .unwrap();
437        let request: super::Request = request
438            .try_into()
439            .expect("failed to convert a non-string header");
440        assert_eq!(request.headers().get("k"), Some("😹"))
441    }
442
443    #[test]
444    fn request_can_be_created() {
445        let req = http_1x::Request::builder()
446            .uri("http://foo.com")
447            .body(SdkBody::from("hello"))
448            .unwrap();
449        let mut req = super::Request::try_from(req).unwrap();
450        req.headers_mut().insert("a", "b");
451        assert_eq!(req.headers().get("a").unwrap(), "b");
452        req.headers_mut().append("a", "c");
453        assert_eq!(req.headers().get("a").unwrap(), "b");
454        let http1 = req.try_into_http1x().unwrap();
455        assert_eq!(http1.uri(), "http://foo.com");
456    }
457
458    #[test]
459    fn uri_mutations() {
460        let req = http_1x::Request::builder()
461            .uri("http://foo.com")
462            .body(SdkBody::from("hello"))
463            .unwrap();
464        let mut req = super::Request::try_from(req).unwrap();
465        assert_eq!(req.uri(), "http://foo.com/");
466        req.set_uri("http://bar.com").unwrap();
467        assert_eq!(req.uri(), "http://bar.com");
468        let http1 = req.try_into_http1x().unwrap();
469        assert_eq!(http1.uri(), "http://bar.com");
470    }
471
472    #[test]
473    fn set_endpoint_merges_paths() {
474        let mut req = super::Request::empty();
475        req.set_uri("/foo/bar").unwrap();
476        req.uri_mut()
477            .set_endpoint("https://www.amazon.com")
478            .unwrap();
479        assert_eq!(req.uri(), "https://www.amazon.com/foo/bar");
480    }
481
482    #[test]
483    #[should_panic]
484    fn header_panics() {
485        let req = http_1x::Request::builder()
486            .uri("http://foo.com")
487            .body(SdkBody::from("hello"))
488            .unwrap();
489        let mut req = super::Request::try_from(req).unwrap();
490        let _ = req
491            .headers_mut()
492            .try_insert("a\nb", "a\nb")
493            .expect_err("invalid header");
494        let _ = req.headers_mut().insert("a\nb", "a\nb");
495    }
496
497    #[test]
498    fn try_clone_clones_all_data() {
499        let request = http_1x::Request::builder()
500            .uri(http_1x::Uri::from_static("https://www.amazon.com"))
501            .method("POST")
502            .header(CONTENT_LENGTH, 456)
503            .header(AUTHORIZATION, "Token: hello")
504            .body(SdkBody::from("hello world!"))
505            .expect("valid request");
506
507        let request: super::Request = request.try_into().unwrap();
508        let cloned = request.try_clone().expect("request is cloneable");
509
510        assert_eq!("https://www.amazon.com/", cloned.uri());
511        assert_eq!("POST", cloned.method());
512        assert_eq!(2, cloned.headers().len());
513        assert_eq!("Token: hello", cloned.headers().get(AUTHORIZATION).unwrap(),);
514        assert_eq!("456", cloned.headers().get(CONTENT_LENGTH).unwrap());
515        assert_eq!("hello world!".as_bytes(), cloned.body().bytes().unwrap());
516    }
517}
518
519#[cfg(all(test, feature = "http-02x", feature = "http-1x"))]
520mod cross_version_test {
521    use super::Request;
522    use aws_smithy_types::body::SdkBody;
523    use http_02x::header::{AUTHORIZATION, CONTENT_LENGTH};
524
525    // The internal URI storage is http 1.x, which accepts some URIs that http 0.2.x rejects.
526    // `try_into_http02x` must surface that as an `Err` rather than panicking in `Uri::into_h0`.
527    #[test]
528    fn converting_a_non_ascii_uri_to_http02x_does_not_panic() {
529        let mut req = Request::empty();
530        if req.set_uri("http://foo.com/\u{80}").is_err() {
531            // If the URI is rejected up front there is nothing to demonstrate.
532            return;
533        }
534        let result =
535            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| req.try_into_http02x()));
536        assert!(
537            result.is_ok(),
538            "Uri::into_h0's `self.as_string.parse()` panicked instead of surfacing an HttpError"
539        );
540    }
541
542    #[test]
543    fn valid_round_trips() {
544        let request = || {
545            http_02x::Request::builder()
546                .uri(http_02x::Uri::from_static("https://www.amazon.com"))
547                .method("POST")
548                .header(CONTENT_LENGTH, 456)
549                .header(AUTHORIZATION, "Token: hello")
550                .header("multi", "v1")
551                .header("multi", "v2")
552                .body(SdkBody::from("hello world!"))
553                .expect("valid request")
554        };
555
556        check_roundtrip(request);
557    }
558
559    macro_rules! req_eq {
560        ($a: expr, $b: expr) => {{
561            assert_eq!($a.uri(), $b.uri(), "status code mismatch");
562            assert_eq!($a.headers(), $b.headers(), "header mismatch");
563            assert_eq!($a.method(), $b.method(), "header mismatch");
564            assert_eq!($a.body().bytes(), $b.body().bytes(), "data mismatch");
565            assert_eq!(
566                $a.extensions().len(),
567                $b.extensions().len(),
568                "extensions size mismatch"
569            );
570        }};
571    }
572
573    #[track_caller]
574    fn check_roundtrip(req: impl Fn() -> http_02x::Request<SdkBody>) {
575        let mut container = super::Request::try_from(req()).unwrap();
576        container.add_extension(5_u32);
577        let mut h1 = container
578            .try_into_http1x()
579            .expect("failed converting to http1x");
580        assert_eq!(h1.extensions().get::<u32>(), Some(&5));
581        h1.extensions_mut().remove::<u32>();
582
583        let mut container = super::Request::try_from(h1).expect("failed converting from http1x");
584        container.add_extension(5_u32);
585        let mut h0 = container
586            .try_into_http02x()
587            .expect("failed converting back to http0x");
588        assert_eq!(h0.extensions().get::<u32>(), Some(&5));
589        h0.extensions_mut().remove::<u32>();
590        req_eq!(h0, req());
591    }
592}