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    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    /// Sets `endpoint` as the endpoint for a URL.
73    ///
74    /// An `endpoint` MUST contain a scheme and authority.
75    /// An `endpoint` MAY contain a port and path.
76    ///
77    /// An `endpoint` MUST NOT contain a query
78    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    /// Returns the URI path.
97    pub fn path(&self) -> &str {
98        self.parsed.path()
99    }
100
101    /// Returns the URI query string.
102    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    /// Converts this request into an http 0.x request.
207    ///
208    /// Depending on the internal storage type, this operation may be free or it may have an internal
209    /// cost.
210    #[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    /// Converts this request into an http 1.x request.
226    ///
227    /// Depending on the internal storage type, this operation may be free or it may have an internal
228    /// cost.
229    #[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    /// Update the body of this request to be a new body.
242    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    /// Returns a GET request with no URI
253    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    /// Convert this request into its parts.
264    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    /// Returns a reference to the header map
273    pub fn headers(&self) -> &Headers {
274        &self.headers
275    }
276
277    /// Returns a mutable reference to the header map
278    pub fn headers_mut(&mut self) -> &mut Headers {
279        &mut self.headers
280    }
281
282    /// Returns the body associated with the request
283    pub fn body(&self) -> &B {
284        &self.body
285    }
286
287    /// Returns a mutable reference to the body
288    pub fn body_mut(&mut self) -> &mut B {
289        &mut self.body
290    }
291
292    /// Converts this request into the request body.
293    pub fn into_body(self) -> B {
294        self.body
295    }
296
297    /// Returns the method associated with this request
298    pub fn method(&self) -> &str {
299        self.method.as_str()
300    }
301
302    /// Sets the HTTP method for this request
303    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    /// Returns the URI associated with this request
310    pub fn uri(&self) -> &str {
311        &self.uri.as_string
312    }
313
314    /// Returns a mutable reference the the URI of this http::Request
315    pub fn uri_mut(&mut self) -> &mut Uri {
316        &mut self.uri
317    }
318
319    /// Sets the URI of this request
320    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    /// Adds an extension to the request extensions
330    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    /// Attempts to clone this request
337    ///
338    /// On clone, any extensions will be cleared.
339    ///
340    /// If the body is cloneable, this will clone the request. Otherwise `None` will be returned
341    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    /// Replaces this request's body with [`SdkBody::taken()`]
353    pub fn take_body(&mut self) -> SdkBody {
354        std::mem::replace(self.body_mut(), SdkBody::taken())
355    }
356
357    /// Create a GET request to `/` with an empty body
358    pub fn empty() -> Self {
359        Self::new(SdkBody::empty())
360    }
361
362    /// Creates a GET request to `uri` with an empty body
363    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}