diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index b4f5b3b8b6..79f95dc718 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -16,3 +16,9 @@ message = "Fix aws-sdk-rust#930 (PutSnapshotBlock)" references = ["smithy-rs#3126", "aws-sdk-rust#930"] meta = { "breaking" = false, "tada" = false, "bug" = true } author = "rcoh" + +[[smithy-rs]] +message = "The HTTP `Request`, `Response`, `Headers`, and `HeaderValue` types have been moved from `aws_smithy_runtime_api::client::http::*` into `aws_smithy_runtime_api::http`" +references = ["smithy-rs#3138"] +meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" } +author = "jdisanti" diff --git a/rust-runtime/aws-smithy-protocol-test/src/lib.rs b/rust-runtime/aws-smithy-protocol-test/src/lib.rs index e1b2f6adfc..92d9f5c4f3 100644 --- a/rust-runtime/aws-smithy-protocol-test/src/lib.rs +++ b/rust-runtime/aws-smithy-protocol-test/src/lib.rs @@ -16,8 +16,8 @@ mod xml; use crate::sealed::GetNormalizedHeader; use crate::xml::try_xml_equivalent; use assert_json_diff::assert_json_eq_no_panic; -use aws_smithy_runtime_api::client::http::request::Headers; use aws_smithy_runtime_api::client::orchestrator::HttpRequest; +use aws_smithy_runtime_api::http::Headers; use http::{HeaderMap, Uri}; use pretty_assertions::Comparison; use std::collections::HashSet; @@ -413,8 +413,8 @@ mod tests { forbid_headers, forbid_query_params, require_headers, require_query_params, validate_body, validate_headers, validate_query_string, FloatEquals, MediaType, ProtocolTestFailure, }; - use aws_smithy_runtime_api::client::http::request::Headers; use aws_smithy_runtime_api::client::orchestrator::HttpRequest; + use aws_smithy_runtime_api::http::Headers; fn make_request(uri: &str) -> HttpRequest { let mut req = HttpRequest::empty(); diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/http.rs b/rust-runtime/aws-smithy-runtime-api/src/client/http.rs index f6e3f03c80..480f74208e 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/http.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/http.rs @@ -50,9 +50,6 @@ //! [`tower`]: https://crates.io/crates/tower //! [`aws-smithy-runtime`]: https://crates.io/crates/aws-smithy-runtime -pub mod request; -pub mod response; - use crate::client::orchestrator::{HttpRequest, HttpResponse}; use crate::client::result::ConnectorError; use crate::client::runtime_components::sealed::ValidateConfig; @@ -62,6 +59,25 @@ use std::fmt; use std::sync::Arc; use std::time::Duration; +/// Http Request Types +pub mod request { + /// Deprecated: This type has moved to `aws_smithy_runtime_api::http::HttpError`. + #[deprecated(note = "This type has moved to `aws_smithy_runtime_api::http::HttpError`.")] + pub type HttpError = crate::http::HttpError; + /// Deprecated: This type has moved to `aws_smithy_runtime_api::http::HeaderValue`. + #[deprecated(note = "This type has moved to `aws_smithy_runtime_api::http::HeaderValue`.")] + pub type HeaderValue = crate::http::HeaderValue; + /// Deprecated: This type has moved to `aws_smithy_runtime_api::http::Headers`. + #[deprecated(note = "This type has moved to `aws_smithy_runtime_api::http::Headers`.")] + pub type Headers = crate::http::Headers; + /// Deprecated: This type has moved to `aws_smithy_runtime_api::http::HeadersIter`. + #[deprecated(note = "This type has moved to `aws_smithy_runtime_api::http::HeadersIter`.")] + pub type HeadersIter<'a> = crate::http::HeadersIter<'a>; + /// Deprecated: This type has moved to `aws_smithy_runtime_api::http::Request`. + #[deprecated(note = "This type has moved to `aws_smithy_runtime_api::http::Request`.")] + pub type Request = crate::http::Request; +} + new_type_future! { #[doc = "Future for [`HttpConnector::call`]."] pub struct HttpConnectorFuture<'static, HttpResponse, ConnectorError>; diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/http/response.rs b/rust-runtime/aws-smithy-runtime-api/src/client/http/response.rs deleted file mode 100644 index 518961d6d1..0000000000 --- a/rust-runtime/aws-smithy-runtime-api/src/client/http/response.rs +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Http Response Types diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs index 4282534a4c..b718bb9116 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/orchestrator.rs @@ -28,7 +28,7 @@ use std::error::Error as StdError; use std::fmt; /// Type alias for the HTTP request type that the orchestrator uses. -pub type HttpRequest = crate::client::http::request::Request; +pub type HttpRequest = crate::http::Request; /// Type alias for the HTTP response type that the orchestrator uses. pub type HttpResponse = http::Response; diff --git a/rust-runtime/aws-smithy-runtime-api/src/http.rs b/rust-runtime/aws-smithy-runtime-api/src/http.rs new file mode 100644 index 0000000000..8d76dc7434 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/http.rs @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! HTTP request and response types + +mod error; +mod headers; +mod request; + +pub use error::HttpError; +pub use headers::{HeaderValue, Headers, HeadersIter}; +pub use request::Request; diff --git a/rust-runtime/aws-smithy-runtime-api/src/http/error.rs b/rust-runtime/aws-smithy-runtime-api/src/http/error.rs new file mode 100644 index 0000000000..8c47334960 --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/http/error.rs @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Error types for HTTP requests/responses. + +use crate::box_error::BoxError; +use http::header::{InvalidHeaderName, InvalidHeaderValue}; +use http::uri::InvalidUri; +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::str::Utf8Error; + +#[derive(Debug)] +/// An error occurred constructing an Http Request. +/// +/// This is normally due to configuration issues, internal SDK bugs, or other user error. +pub struct HttpError(BoxError); + +impl HttpError { + // TODO(httpRefactor): Add better error internals + pub(super) fn new>>(err: E) -> Self { + HttpError(err.into()) + } + + pub(super) fn invalid_header_value(err: InvalidHeaderValue) -> Self { + Self(err.into()) + } + + pub(super) fn header_was_not_a_string(err: Utf8Error) -> Self { + Self(err.into()) + } + + pub(super) fn invalid_header_name(err: InvalidHeaderName) -> Self { + Self(err.into()) + } + + pub(super) fn invalid_uri(err: InvalidUri) -> Self { + Self(err.into()) + } +} + +impl Display for HttpError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "an error occurred creating an HTTP Request") + } +} + +impl Error for HttpError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(self.0.as_ref()) + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/http/headers.rs b/rust-runtime/aws-smithy-runtime-api/src/http/headers.rs new file mode 100644 index 0000000000..e0e1457c7a --- /dev/null +++ b/rust-runtime/aws-smithy-runtime-api/src/http/headers.rs @@ -0,0 +1,343 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Types for HTTP headers + +use crate::http::error::HttpError; +use http as http0; +use http0::header::Iter; +use http0::HeaderMap; +use std::borrow::Cow; +use std::fmt::Debug; +use std::str::FromStr; + +/// An immutable view of headers +#[derive(Clone, Default, Debug)] +pub struct Headers { + pub(super) headers: HeaderMap, +} + +impl<'a> IntoIterator for &'a Headers { + type Item = (&'a str, &'a str); + type IntoIter = HeadersIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + HeadersIter { + inner: self.headers.iter(), + } + } +} + +/// An Iterator over headers +pub struct HeadersIter<'a> { + inner: Iter<'a, HeaderValue>, +} + +impl<'a> Iterator for HeadersIter<'a> { + type Item = (&'a str, &'a str); + + fn next(&mut self) -> Option { + self.inner.next().map(|(k, v)| (k.as_str(), v.as_ref())) + } +} + +impl Headers { + /// Create an empty header map + pub fn new() -> Self { + Self::default() + } + + /// Returns the value for a given key + /// + /// If multiple values are associated, the first value is returned + /// See [HeaderMap::get] + pub fn get(&self, key: impl AsRef) -> Option<&str> { + self.headers.get(key.as_ref()).map(|v| v.as_ref()) + } + + /// Returns all values for a given key + pub fn get_all(&self, key: impl AsRef) -> impl Iterator { + self.headers + .get_all(key.as_ref()) + .iter() + .map(|v| v.as_ref()) + } + + /// Returns an iterator over the headers + pub fn iter(&self) -> HeadersIter<'_> { + HeadersIter { + inner: self.headers.iter(), + } + } + + /// Returns the total number of **values** stored in the map + pub fn len(&self) -> usize { + self.headers.len() + } + + /// Returns true if there are no headers + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns true if this header is present + pub fn contains_key(&self, key: &str) -> bool { + self.headers.contains_key(key) + } + + /// Insert a value into the headers structure. + /// + /// This will *replace* any existing value for this key. Returns the previous associated value if any. + /// + /// # Panics + /// If the key or value are not valid ascii, this function will panic. + pub fn insert( + &mut self, + key: impl AsHeaderComponent, + value: impl AsHeaderComponent, + ) -> Option { + self.try_insert(key, value) + .expect("HeaderName or HeaderValue was invalid") + } + + /// Insert a value into the headers structure. + /// + /// This will *replace* any existing value for this key. Returns the previous associated value if any. + /// + /// If the key or value are not valid ascii, an error is returned + pub fn try_insert( + &mut self, + key: impl AsHeaderComponent, + value: impl AsHeaderComponent, + ) -> Result, HttpError> { + let key = header_name(key)?; + let value = header_value(value.into_maybe_static()?)?; + Ok(self + .headers + .insert(key, value) + .map(|old_value| old_value.into())) + } + + /// Appends a value to a given key + /// + /// If the key or value are NOT valid ascii, an error is returned + pub fn try_append( + &mut self, + key: impl AsHeaderComponent, + value: impl AsHeaderComponent, + ) -> Result { + let key = header_name(key.into_maybe_static()?)?; + let value = header_value(value.into_maybe_static()?)?; + Ok(self.headers.append(key, value)) + } + + /// Removes all headers with a given key + /// + /// If there are multiple entries for this key, the first entry is returned + pub fn remove(&mut self, key: impl AsRef) -> Option { + self.headers + .remove(key.as_ref()) + .map(|h| h.as_str().to_string()) + } + + /// Appends a value to a given key + /// + /// # Panics + /// If the key or value are NOT valid ascii, this function will panic + pub fn append(&mut self, key: impl AsHeaderComponent, value: impl AsHeaderComponent) -> bool { + self.try_append(key, value) + .expect("HeaderName or HeaderValue was invalid") + } +} + +use sealed::AsHeaderComponent; + +mod sealed { + use super::*; + /// Trait defining things that may be converted into a header component (name or value) + pub trait AsHeaderComponent { + /// If the component can be represented as a Cow<'static, str>, return it + fn into_maybe_static(self) -> Result; + + /// Return a string reference to this header + fn as_str(&self) -> Result<&str, HttpError>; + + /// If a component is already internally represented as a `http02x::HeaderName`, return it + fn repr_as_http02x_header_name(self) -> Result + where + Self: Sized, + { + Err(self) + } + } + + impl AsHeaderComponent for &'static str { + fn into_maybe_static(self) -> Result { + Ok(Cow::Borrowed(self)) + } + + fn as_str(&self) -> Result<&str, HttpError> { + Ok(self) + } + } + + impl AsHeaderComponent for String { + fn into_maybe_static(self) -> Result { + Ok(Cow::Owned(self)) + } + + fn as_str(&self) -> Result<&str, HttpError> { + Ok(self) + } + } + + impl AsHeaderComponent for Cow<'static, str> { + fn into_maybe_static(self) -> Result { + Ok(self) + } + + fn as_str(&self) -> Result<&str, HttpError> { + Ok(self.as_ref()) + } + } + + impl AsHeaderComponent for http0::HeaderValue { + fn into_maybe_static(self) -> Result { + Ok(Cow::Owned( + std::str::from_utf8(self.as_bytes()) + .map_err(HttpError::header_was_not_a_string)? + .to_string(), + )) + } + + fn as_str(&self) -> Result<&str, HttpError> { + std::str::from_utf8(self.as_bytes()).map_err(HttpError::header_was_not_a_string) + } + } + + impl AsHeaderComponent for http0::HeaderName { + fn into_maybe_static(self) -> Result { + Ok(self.to_string().into()) + } + + fn as_str(&self) -> Result<&str, HttpError> { + Ok(self.as_ref()) + } + + fn repr_as_http02x_header_name(self) -> Result + where + Self: Sized, + { + Ok(self) + } + } +} + +mod header_value { + use super::*; + use std::str::Utf8Error; + + /// HeaderValue type + /// + /// **Note**: Unlike `HeaderValue` in `http`, this only supports UTF-8 header values + #[derive(Debug, Clone)] + pub struct HeaderValue { + _private: http0::HeaderValue, + } + + impl HeaderValue { + pub(crate) fn from_http02x(value: http0::HeaderValue) -> Result { + let _ = std::str::from_utf8(value.as_bytes())?; + Ok(Self { _private: value }) + } + + pub(crate) fn into_http02x(self) -> http0::HeaderValue { + self._private + } + } + + impl AsRef for HeaderValue { + fn as_ref(&self) -> &str { + std::str::from_utf8(self._private.as_bytes()) + .expect("unreachable—only strings may be stored") + } + } + + impl From for String { + fn from(value: HeaderValue) -> Self { + value.as_ref().to_string() + } + } + + impl HeaderValue { + /// Returns the string representation of this header value + pub fn as_str(&self) -> &str { + self.as_ref() + } + } + + impl FromStr for HeaderValue { + type Err = HttpError; + + fn from_str(s: &str) -> Result { + HeaderValue::try_from(s.to_string()) + } + } + + impl TryFrom for HeaderValue { + type Error = HttpError; + + fn try_from(value: String) -> Result { + Ok(HeaderValue::from_http02x( + http0::HeaderValue::try_from(value).map_err(HttpError::invalid_header_value)?, + ) + .expect("input was a string")) + } + } +} + +pub use header_value::HeaderValue; + +type MaybeStatic = Cow<'static, str>; + +fn header_name(name: impl AsHeaderComponent) -> Result { + name.repr_as_http02x_header_name().or_else(|name| { + name.into_maybe_static().and_then(|cow| { + if cow.chars().any(|c| c.is_uppercase()) { + return Err(HttpError::new("Header names must be all lower case")); + } + match cow { + Cow::Borrowed(staticc) => Ok(http0::HeaderName::from_static(staticc)), + Cow::Owned(s) => { + http0::HeaderName::try_from(s).map_err(HttpError::invalid_header_name) + } + } + }) + }) +} + +fn header_value(value: MaybeStatic) -> Result { + let header = match value { + Cow::Borrowed(b) => http0::HeaderValue::from_static(b), + Cow::Owned(s) => { + http0::HeaderValue::try_from(s).map_err(HttpError::invalid_header_value)? + } + }; + HeaderValue::from_http02x(header).map_err(HttpError::new) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn headers_can_be_any_string() { + let _: HeaderValue = "😹".parse().expect("can be any string"); + let _: HeaderValue = "abcd".parse().expect("can be any string"); + let _ = "a\nb" + .parse::() + .expect_err("cannot contain control characters"); + } +} diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/http/request.rs b/rust-runtime/aws-smithy-runtime-api/src/http/request.rs similarity index 50% rename from rust-runtime/aws-smithy-runtime-api/src/client/http/request.rs rename to rust-runtime/aws-smithy-runtime-api/src/http/request.rs index 5b4172160d..b75ea583b7 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/http/request.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/http/request.rs @@ -5,17 +5,13 @@ //! Http Request Types +use crate::http::error::HttpError; +use crate::http::headers::{HeaderValue, Headers}; use aws_smithy_types::body::SdkBody; use http as http0; -use http::header::{InvalidHeaderName, InvalidHeaderValue}; -use http::uri::InvalidUri; -use http0::header::Iter; use http0::uri::PathAndQuery; use http0::{Extensions, HeaderMap, Method}; use std::borrow::Cow; -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; -use std::str::{FromStr, Utf8Error}; #[derive(Debug)] /// An HTTP Request Type @@ -230,7 +226,7 @@ impl Request { }) } - /// Replaces this requests body with [`SdkBody::taken()`] + /// Replaces this request's body with [`SdkBody::taken()`] pub fn take_body(&mut self) -> SdkBody { std::mem::replace(self.body_mut(), SdkBody::taken()) } @@ -281,378 +277,12 @@ impl TryFrom> for Request { } } -/// An immutable view of request headers -#[derive(Clone, Default, Debug)] -pub struct Headers { - headers: HeaderMap, -} - -impl<'a> IntoIterator for &'a Headers { - type Item = (&'a str, &'a str); - type IntoIter = HeadersIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - HeadersIter { - inner: self.headers.iter(), - } - } -} - -/// An Iterator over headers -pub struct HeadersIter<'a> { - inner: Iter<'a, HeaderValue>, -} - -impl<'a> Iterator for HeadersIter<'a> { - type Item = (&'a str, &'a str); - - fn next(&mut self) -> Option { - self.inner.next().map(|(k, v)| (k.as_str(), v.as_ref())) - } -} - -impl Headers { - /// Create an empty header map - pub fn new() -> Self { - Self::default() - } - - /// Returns the value for a given key - /// - /// If multiple values are associated, the first value is returned - /// See [HeaderMap::get] - pub fn get(&self, key: impl AsRef) -> Option<&str> { - self.headers.get(key.as_ref()).map(|v| v.as_ref()) - } - - /// Returns all values for a given key - pub fn get_all(&self, key: impl AsRef) -> impl Iterator { - self.headers - .get_all(key.as_ref()) - .iter() - .map(|v| v.as_ref()) - } - - /// Returns an iterator over the headers - pub fn iter(&self) -> HeadersIter<'_> { - HeadersIter { - inner: self.headers.iter(), - } - } - - /// Returns the total number of **values** stored in the map - pub fn len(&self) -> usize { - self.headers.len() - } - - /// Returns true if there are no headers - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns true if this header is present - pub fn contains_key(&self, key: &str) -> bool { - self.headers.contains_key(key) - } - - /// Insert a value into the headers structure. - /// - /// This will *replace* any existing value for this key. Returns the previous associated value if any. - /// - /// # Panics - /// If the key or value are not valid ascii, this function will panic. - pub fn insert( - &mut self, - key: impl AsHeaderComponent, - value: impl AsHeaderComponent, - ) -> Option { - self.try_insert(key, value) - .expect("HeaderName or HeaderValue was invalid") - } - - /// Insert a value into the headers structure. - /// - /// This will *replace* any existing value for this key. Returns the previous associated value if any. - /// - /// If the key or value are not valid ascii, an error is returned - pub fn try_insert( - &mut self, - key: impl AsHeaderComponent, - value: impl AsHeaderComponent, - ) -> Result, HttpError> { - let key = header_name(key)?; - let value = header_value(value.into_maybe_static()?)?; - Ok(self - .headers - .insert(key, value) - .map(|old_value| old_value.into())) - } - - /// Appends a value to a given key - /// - /// If the key or value are NOT valid ascii, an error is returned - pub fn try_append( - &mut self, - key: impl AsHeaderComponent, - value: impl AsHeaderComponent, - ) -> Result { - let key = header_name(key.into_maybe_static()?)?; - let value = header_value(value.into_maybe_static()?)?; - Ok(self.headers.append(key, value)) - } - - /// Removes all headers with a given key - /// - /// If there are multiple entries for this key, the first entry is returned - pub fn remove(&mut self, key: impl AsRef) -> Option { - self.headers - .remove(key.as_ref()) - .map(|h| h.as_str().to_string()) - } - - /// Appends a value to a given key - /// - /// # Panics - /// If the key or value are NOT valid ascii, this function will panic - pub fn append(&mut self, key: impl AsHeaderComponent, value: impl AsHeaderComponent) -> bool { - self.try_append(key, value) - .expect("HeaderName or HeaderValue was invalid") - } -} - -use sealed::AsHeaderComponent; - -mod sealed { - use super::*; - /// Trait defining things that may be converted into a header component (name or value) - pub trait AsHeaderComponent { - /// If the component can be represented as a Cow<'static, str>, return it - fn into_maybe_static(self) -> Result; - - /// Return a string reference to this header - fn as_str(&self) -> Result<&str, HttpError>; - - /// If a component is already internally represented as a `http02x::HeaderName`, return it - fn repr_as_http02x_header_name(self) -> Result - where - Self: Sized, - { - Err(self) - } - } - - impl AsHeaderComponent for &'static str { - fn into_maybe_static(self) -> Result { - Ok(Cow::Borrowed(self)) - } - - fn as_str(&self) -> Result<&str, HttpError> { - Ok(self) - } - } - - impl AsHeaderComponent for String { - fn into_maybe_static(self) -> Result { - Ok(Cow::Owned(self)) - } - - fn as_str(&self) -> Result<&str, HttpError> { - Ok(self) - } - } - - impl AsHeaderComponent for Cow<'static, str> { - fn into_maybe_static(self) -> Result { - Ok(self) - } - - fn as_str(&self) -> Result<&str, HttpError> { - Ok(self.as_ref()) - } - } - - impl AsHeaderComponent for http0::HeaderValue { - fn into_maybe_static(self) -> Result { - Ok(Cow::Owned( - std::str::from_utf8(self.as_bytes()) - .map_err(HttpError::header_was_not_a_string)? - .to_string(), - )) - } - - fn as_str(&self) -> Result<&str, HttpError> { - std::str::from_utf8(self.as_bytes()).map_err(HttpError::header_was_not_a_string) - } - } - - impl AsHeaderComponent for http0::HeaderName { - fn into_maybe_static(self) -> Result { - Ok(self.to_string().into()) - } - - fn as_str(&self) -> Result<&str, HttpError> { - Ok(self.as_ref()) - } - - fn repr_as_http02x_header_name(self) -> Result - where - Self: Sized, - { - Ok(self) - } - } -} - -mod header_value { - use super::http0; - use std::str::Utf8Error; - - /// HeaderValue type - /// - /// **Note**: Unlike `HeaderValue` in `http`, this only supports UTF-8 header values - #[derive(Debug, Clone)] - pub struct HeaderValue { - _private: http0::HeaderValue, - } - - impl HeaderValue { - pub(crate) fn from_http02x(value: http0::HeaderValue) -> Result { - let _ = std::str::from_utf8(value.as_bytes())?; - Ok(Self { _private: value }) - } - - pub(crate) fn into_http02x(self) -> http0::HeaderValue { - self._private - } - } - - impl AsRef for HeaderValue { - fn as_ref(&self) -> &str { - std::str::from_utf8(self._private.as_bytes()) - .expect("unreachable—only strings may be stored") - } - } - - impl From for String { - fn from(value: HeaderValue) -> Self { - value.as_ref().to_string() - } - } -} - -use crate::box_error::BoxError; -pub use header_value::HeaderValue; - -impl HeaderValue { - /// Returns the string representation of this header value - pub fn as_str(&self) -> &str { - self.as_ref() - } -} - -impl FromStr for HeaderValue { - type Err = HttpError; - - fn from_str(s: &str) -> Result { - HeaderValue::try_from(s.to_string()) - } -} - -impl TryFrom for HeaderValue { - type Error = HttpError; - - fn try_from(value: String) -> Result { - Ok(HeaderValue::from_http02x( - http0::HeaderValue::try_from(value).map_err(HttpError::invalid_header_value)?, - ) - .expect("input was a string")) - } -} - -type MaybeStatic = Cow<'static, str>; - -#[derive(Debug)] -/// An error occurred constructing an Http Request. -/// -/// This is normally due to configuration issues, internal SDK bugs, or other user error. -pub struct HttpError(BoxError); - -impl HttpError { - // TODO(httpRefactor): Add better error internals - fn new>>(err: E) -> Self { - HttpError(err.into()) - } - - fn invalid_header_value(err: InvalidHeaderValue) -> Self { - Self(err.into()) - } - - fn header_was_not_a_string(err: Utf8Error) -> Self { - Self(err.into()) - } - - fn invalid_header_name(err: InvalidHeaderName) -> Self { - Self(err.into()) - } - - fn invalid_uri(err: InvalidUri) -> Self { - Self(err.into()) - } -} - -impl Display for HttpError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "an error occurred creating an HTTP Request") - } -} - -impl Error for HttpError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(self.0.as_ref()) - } -} - -fn header_name(name: impl AsHeaderComponent) -> Result { - name.repr_as_http02x_header_name().or_else(|name| { - name.into_maybe_static().and_then(|cow| { - if cow.chars().any(|c| c.is_uppercase()) { - return Err(HttpError::new("Header names must be all lower case")); - } - match cow { - Cow::Borrowed(staticc) => Ok(http0::HeaderName::from_static(staticc)), - Cow::Owned(s) => { - http0::HeaderName::try_from(s).map_err(HttpError::invalid_header_name) - } - } - }) - }) -} - -fn header_value(value: MaybeStatic) -> Result { - let header = match value { - Cow::Borrowed(b) => http0::HeaderValue::from_static(b), - Cow::Owned(s) => { - http0::HeaderValue::try_from(s).map_err(HttpError::invalid_header_value)? - } - }; - HeaderValue::from_http02x(header).map_err(HttpError::new) -} - #[cfg(test)] mod test { - use crate::client::orchestrator::HttpRequest; + use super::*; use aws_smithy_types::body::SdkBody; use http::header::{AUTHORIZATION, CONTENT_LENGTH}; - use http::{HeaderValue, Uri}; - - #[test] - fn headers_can_be_any_string() { - let _: HeaderValue = "😹".parse().expect("can be any string"); - let _: HeaderValue = "abcd".parse().expect("can be any string"); - let _ = "a\nb" - .parse::() - .expect_err("cannot contain control characters"); - } + use http::Uri; #[test] fn non_ascii_requests() { @@ -660,7 +290,7 @@ mod test { .header("k", "😹") .body(SdkBody::empty()) .unwrap(); - let request: HttpRequest = request + let request: Request = request .try_into() .expect("failed to convert a non-string header"); assert_eq!(request.headers().get("k"), Some("😹")) diff --git a/rust-runtime/aws-smithy-runtime-api/src/lib.rs b/rust-runtime/aws-smithy-runtime-api/src/lib.rs index 56b65f209a..9a7897d6dc 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/lib.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/lib.rs @@ -31,6 +31,8 @@ pub mod box_error; #[cfg(feature = "client")] pub mod client; +pub mod http; + /// Internal builder macros. Not intended to be used outside of the aws-smithy-runtime crates. #[doc(hidden)] pub mod macros; diff --git a/rust-runtime/aws-smithy-runtime/src/client/http/test_util/dvr.rs b/rust-runtime/aws-smithy-runtime/src/client/http/test_util/dvr.rs index 5f50faaed7..1fdf61ac4d 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/http/test_util/dvr.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/http/test_util/dvr.rs @@ -9,6 +9,8 @@ //! //! DVR is an extremely experimental record & replay framework that supports multi-frame HTTP request / response traffic. +use aws_smithy_runtime_api::client::orchestrator::HttpRequest; +use aws_smithy_runtime_api::http::Headers; use aws_smithy_types::base64; use bytes::Bytes; use http::HeaderMap; @@ -19,8 +21,6 @@ mod record; mod replay; pub use aws_smithy_protocol_test::MediaType; -use aws_smithy_runtime_api::client::http::request::Headers; -use aws_smithy_runtime_api::client::orchestrator::HttpRequest; pub use record::RecordingClient; pub use replay::ReplayingClient;