From 32bd9fc402d6f0ff728eaf7deb57d11cbd80fa3e Mon Sep 17 00:00:00 2001 From: Russell Cohen Date: Tue, 16 Jan 2024 16:48:05 -0500 Subject: [PATCH] Add http = "1.0" support to the `http` request wrapper - Requests are not creatible if extensions exist avoiding the cross-version extension issue. - Response _were_ creatable. For responses, the wrapper will error if extensions existed and you attempt to cross convert Http versions. --- CHANGELOG.next.toml | 12 + .../aws-smithy-runtime-api/Cargo.toml | 4 +- .../aws-smithy-runtime-api/src/http/error.rs | 5 + .../src/http/headers.rs | 91 +++++++- .../src/http/request.rs | 185 +++++++++++++-- .../src/http/response.rs | 210 ++++++++++++++---- rust-runtime/aws-smithy-runtime/Cargo.toml | 3 +- .../src/client/http/test_util/replay.rs | 20 ++ 8 files changed, 455 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 8a30e72ab4..2933b59967 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -34,3 +34,15 @@ message = "Fix bug where overriding the credentials at the operation level faile references = ["aws-sdk-rust#901", "smithy-rs#3363"] meta = { "breaking" = false, "bug" = true, "tada" = false } author = "rcoh" + +[[smithy-rs]] +message = "Add `try_into_http1x` and `try_from_http1x` to Request and Response container types." +references = ["aws-sdk-rust#977", "smithy-rs#3365", "smithy-rs#3373"] +meta = { "breaking" = false, "bug" = false, "tada" = false, "target" = "all" } +author = "rcoh" + +[[aws-sdk-rust]] +message = "Add `try_into_http1x` and `try_from_http1x` to Request and Response container types." +references = ["aws-sdk-rust#977", "smithy-rs#3365", "smithy-rs#3373"] +meta = { "breaking" = false, "bug" = false, "tada" = false } +author = "rcoh" diff --git a/rust-runtime/aws-smithy-runtime-api/Cargo.toml b/rust-runtime/aws-smithy-runtime-api/Cargo.toml index aed9bc45e2..45cb2d6e12 100644 --- a/rust-runtime/aws-smithy-runtime-api/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime-api/Cargo.toml @@ -13,14 +13,16 @@ repository = "https://github.com/smithy-lang/smithy-rs" default = [] client = [] http-auth = ["dep:zeroize"] -test-util = ["aws-smithy-types/test-util"] +test-util = ["aws-smithy-types/test-util", "http-1x"] http-02x = [] +http-1x = [] [dependencies] aws-smithy-async = { path = "../aws-smithy-async" } aws-smithy-types = { path = "../aws-smithy-types" } bytes = "1" http = "0.2.9" +http1 = { package = "http", version = "1" } pin-project-lite = "0.2" tokio = { version = "1.25", features = ["sync"] } tracing = "0.1" diff --git a/rust-runtime/aws-smithy-runtime-api/src/http/error.rs b/rust-runtime/aws-smithy-runtime-api/src/http/error.rs index 4a0963cbb3..72ba9869a9 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/http/error.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/http/error.rs @@ -24,6 +24,11 @@ impl HttpError { HttpError(err.into()) } + #[allow(dead_code)] + pub(super) fn invalid_extensions() -> Self { + Self("Extensions were provided during initialization. This prevents the request format from being converted.".into()) + } + pub(super) fn invalid_header_value(err: InvalidHeaderValue) -> Self { Self(err.into()) } diff --git a/rust-runtime/aws-smithy-runtime-api/src/http/headers.rs b/rust-runtime/aws-smithy-runtime-api/src/http/headers.rs index 0a2dfd617e..0c58358a0d 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/http/headers.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/http/headers.rs @@ -49,6 +49,29 @@ impl Headers { Self::default() } + #[cfg(feature = "http-1x")] + pub(crate) fn http1_headermap(self) -> http1::HeaderMap { + let mut headers = http1::HeaderMap::new(); + headers.reserve(self.headers.len()); + headers.extend(self.headers.into_iter().map(|(k, v)| { + ( + k.map(|n| { + http1::HeaderName::from_bytes(n.as_str().as_bytes()).expect("proven valid") + }), + v.into_http1x(), + ) + })); + headers + } + + #[cfg(feature = "http-02x")] + pub(crate) fn http0_headermap(self) -> http0::HeaderMap { + let mut headers = http0::HeaderMap::new(); + headers.reserve(self.headers.len()); + headers.extend(self.headers.into_iter().map(|(k, v)| (k, v.into_http02x()))); + headers + } + /// Returns the value for a given key /// /// If multiple values are associated, the first value is returned @@ -181,6 +204,34 @@ impl TryFrom for Headers { } } +#[cfg(feature = "http-1x")] +impl TryFrom for Headers { + type Error = HttpError; + + fn try_from(value: http1::HeaderMap) -> Result { + if let Some(e) = value + .values() + .filter_map(|value| std::str::from_utf8(value.as_bytes()).err()) + .next() + { + Err(HttpError::header_was_not_a_string(e)) + } else { + let mut string_safe_headers: http0::HeaderMap = Default::default(); + string_safe_headers.extend(value.into_iter().map(|(k, v)| { + ( + k.map(|v| { + http0::HeaderName::from_bytes(v.as_str().as_bytes()).expect("known valid") + }), + HeaderValue::from_http1x(v).expect("validated above"), + ) + })); + Ok(Headers { + headers: string_safe_headers, + }) + } + } +} + use sealed::AsHeaderComponent; mod sealed { @@ -273,25 +324,55 @@ mod header_value { /// **Note**: Unlike `HeaderValue` in `http`, this only supports UTF-8 header values #[derive(Debug, Clone)] pub struct HeaderValue { - _private: http0::HeaderValue, + _private: Inner, + } + + #[derive(Debug, Clone)] + enum Inner { + H0(http0::HeaderValue), + #[allow(dead_code)] + H1(http1::HeaderValue), } impl HeaderValue { pub(crate) fn from_http02x(value: http0::HeaderValue) -> Result { let _ = std::str::from_utf8(value.as_bytes())?; - Ok(Self { _private: value }) + Ok(Self { + _private: Inner::H0(value), + }) + } + + pub(crate) fn from_http1x(value: http1::HeaderValue) -> Result { + let _ = std::str::from_utf8(value.as_bytes())?; + Ok(Self { + _private: Inner::H1(value), + }) } #[allow(dead_code)] pub(crate) fn into_http02x(self) -> http0::HeaderValue { - self._private + match self._private { + Inner::H0(v) => v, + Inner::H1(v) => http0::HeaderValue::from_maybe_shared(v).expect("unreachable"), + } + } + + #[allow(dead_code)] + pub(crate) fn into_http1x(self) -> http1::HeaderValue { + match self._private { + Inner::H1(v) => v, + Inner::H0(v) => http1::HeaderValue::from_maybe_shared(v).expect("unreachable"), + } } } impl AsRef for HeaderValue { fn as_ref(&self) -> &str { - std::str::from_utf8(self._private.as_bytes()) - .expect("unreachable—only strings may be stored") + let bytes = match &self._private { + Inner::H0(v) => v.as_bytes(), + Inner::H1(v) => v.as_bytes(), + }; + std::str::from_utf8(bytes).expect("unreachable—only strings may be stored") } } diff --git a/rust-runtime/aws-smithy-runtime-api/src/http/request.rs b/rust-runtime/aws-smithy-runtime-api/src/http/request.rs index ee87e57f1d..f83e788228 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/http/request.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/http/request.rs @@ -31,6 +31,7 @@ pub struct Request { uri: Uri, method: Method, extensions_02x: Extensions, + extensions_1x: http1::Extensions, headers: Headers, } @@ -38,7 +39,36 @@ pub struct Request { #[derive(Debug, Clone)] pub struct Uri { as_string: String, - parsed: http0::Uri, + parsed: ParsedUri, +} + +#[derive(Debug, Clone)] +enum ParsedUri { + H0(http0::Uri), + H1(http1::Uri), +} + +impl ParsedUri { + fn path_and_query(&self) -> &str { + match &self { + ParsedUri::H0(u) => u.path_and_query().map(|pq| pq.as_str()).unwrap_or(""), + ParsedUri::H1(u) => u.path_and_query().map(|pq| pq.as_str()).unwrap_or(""), + } + } + + fn path(&self) -> &str { + match &self { + ParsedUri::H0(u) => u.path(), + ParsedUri::H1(u) => u.path(), + } + } + + fn query(&self) -> Option<&str> { + match &self { + ParsedUri::H0(u) => u.query(), + ParsedUri::H1(u) => u.query(), + } + } } impl Uri { @@ -64,7 +94,7 @@ impl Uri { .build() .map_err(HttpError::new)?; self.as_string = new_uri.to_string(); - self.parsed = new_uri; + self.parsed = ParsedUri::H0(new_uri); Ok(()) } @@ -81,13 +111,28 @@ impl Uri { fn from_http0x_uri(uri: http0::Uri) -> Self { Self { as_string: uri.to_string(), - parsed: uri, + parsed: ParsedUri::H0(uri), + } + } + + #[allow(dead_code)] + fn from_http1x_uri(uri: http1::Uri) -> Self { + Self { + as_string: uri.to_string(), + parsed: ParsedUri::H1(uri), + } + } + + fn into_h0(self) -> http0::Uri { + match self.parsed { + ParsedUri::H0(uri) => uri, + ParsedUri::H1(_uri) => self.as_string.parse().unwrap(), } } } -fn merge_paths(endpoint_path: Option, uri: &http0::Uri) -> Cow<'_, str> { - let uri_path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or(""); +fn merge_paths(endpoint_path: Option, uri: &ParsedUri) -> Cow<'_, str> { + let uri_path_and_query = uri.path_and_query(); let endpoint_path = match endpoint_path { None => return Cow::Borrowed(uri_path_and_query), Some(path) => path, @@ -111,7 +156,7 @@ impl TryFrom for Uri { type Error = HttpError; fn try_from(value: String) -> Result { - let parsed = value.parse().map_err(HttpError::invalid_uri)?; + let parsed = ParsedUri::H0(value.parse().map_err(HttpError::invalid_uri)?); Ok(Uri { as_string: value, parsed, @@ -142,6 +187,15 @@ impl TryInto> for Request { } } +#[cfg(feature = "http-1x")] +impl TryInto> for Request { + type Error = HttpError; + + fn try_into(self) -> Result, Self::Error> { + self.try_into_http1x() + } +} + impl Request { /// Converts this request into an http 0.x request. /// @@ -150,23 +204,31 @@ impl Request { #[cfg(feature = "http-02x")] pub fn try_into_http02x(self) -> Result, HttpError> { let mut req = http::Request::builder() - .uri(self.uri.parsed) + .uri(self.uri.into_h0()) .method(self.method) .body(self.body) .expect("known valid"); - let mut headers = http0::HeaderMap::new(); - headers.reserve(self.headers.headers.len()); - headers.extend( - self.headers - .headers - .into_iter() - .map(|(k, v)| (k, v.into_http02x())), - ); - *req.headers_mut() = headers; + *req.headers_mut() = self.headers.http0_headermap(); *req.extensions_mut() = self.extensions_02x; Ok(req) } + /// Converts this request into an http 1.x request. + /// + /// Depending on the internal storage type, this operation may be free or it may have an internal + /// cost. + #[cfg(feature = "http-1x")] + pub fn try_into_http1x(self) -> Result, HttpError> { + let mut req = http1::Request::builder() + .uri(self.uri.as_string) + .method(self.method.as_str()) + .body(self.body) + .expect("known valid"); + *req.headers_mut() = self.headers.http1_headermap(); + *req.extensions_mut() = self.extensions_1x; + Ok(req) + } + /// Update the body of this request to be a new body. pub fn map(self, f: impl Fn(B) -> U) -> Request { Request { @@ -174,6 +236,7 @@ impl Request { uri: self.uri, method: self.method, extensions_02x: self.extensions_02x, + extensions_1x: self.extensions_1x, headers: self.headers, } } @@ -185,6 +248,7 @@ impl Request { uri: Uri::from_http0x_uri(http0::Uri::from_static("/")), method: Method::GET, extensions_02x: Default::default(), + extensions_1x: Default::default(), headers: Default::default(), } } @@ -250,7 +314,8 @@ impl Request { /// Adds an extension to the request extensions pub fn add_extension(&mut self, extension: T) { - self.extensions_02x.insert(extension); + self.extensions_02x.insert(extension.clone()); + self.extensions_1x.insert(extension); } } @@ -267,6 +332,7 @@ impl Request { uri: self.uri.clone(), method: self.method.clone(), extensions_02x: Extensions::new(), + extensions_1x: Default::default(), headers: self.headers.clone(), }) } @@ -297,22 +363,47 @@ impl TryFrom> for Request { let (parts, body) = value.into_parts(); let headers = Headers::try_from(parts.headers)?; // we need to do this eventually. - /*if !parts.extensions.is_empty() { + if !parts.extensions.is_empty() { return Err(HttpError::new( "Cannot convert non-empty extensions. Clear extensions before converting", )); - }*/ + } Ok(Self { body, uri: parts.uri.into(), method: parts.method, - extensions_02x: http::Extensions::new(), + extensions_02x: Default::default(), + extensions_1x: Default::default(), + headers, + }) + } +} + +#[cfg(feature = "http-1x")] +impl TryFrom> for Request { + type Error = HttpError; + + fn try_from(value: http1::Request) -> Result { + let (parts, body) = value.into_parts(); + let headers = Headers::try_from(parts.headers)?; + // we need to do this eventually. + if !parts.extensions.is_empty() { + return Err(HttpError::new( + "Cannot convert non-empty extensions. Clear extensions before converting", + )); + } + Ok(Self { + body, + uri: Uri::from_http1x_uri(parts.uri), + method: Method::from_bytes(parts.method.as_str().as_bytes()).expect("valid"), + extensions_02x: Default::default(), + extensions_1x: Default::default(), headers, }) } } -#[cfg(all(test, feature = "http-02x"))] +#[cfg(all(test, feature = "http-02x", feature = "http-1x"))] mod test { use super::*; use aws_smithy_types::body::SdkBody; @@ -384,6 +475,7 @@ mod test { .header(AUTHORIZATION, "Token: hello") .body(SdkBody::from("hello world!")) .expect("valid request"); + let request: super::Request = request.try_into().unwrap(); let cloned = request.try_clone().expect("request is cloneable"); @@ -394,4 +486,55 @@ mod test { assert_eq!("456", cloned.headers().get(CONTENT_LENGTH).unwrap()); assert_eq!("hello world!".as_bytes(), cloned.body().bytes().unwrap()); } + + #[test] + fn valid_round_trips() { + let request = || { + http::Request::builder() + .uri(Uri::from_static("https://www.amazon.com")) + .method("POST") + .header(CONTENT_LENGTH, 456) + .header(AUTHORIZATION, "Token: hello") + .header("multi", "v1") + .header("multi", "v2") + .body(SdkBody::from("hello world!")) + .expect("valid request") + }; + + check_roundtrip(request); + } + + macro_rules! req_eq { + ($a: expr, $b: expr) => {{ + assert_eq!($a.uri(), $b.uri(), "status code mismatch"); + assert_eq!($a.headers(), $b.headers(), "header mismatch"); + assert_eq!($a.method(), $b.method(), "header mismatch"); + assert_eq!($a.body().bytes(), $b.body().bytes(), "data mismatch"); + assert_eq!( + $a.extensions().len(), + $b.extensions().len(), + "extensions size mismatch" + ); + }}; + } + + #[track_caller] + fn check_roundtrip(req: impl Fn() -> http0::Request) { + let mut container = super::Request::try_from(req()).unwrap(); + container.add_extension(5_u32); + let mut h1 = container + .try_into_http1x() + .expect("failed converting to http1x"); + assert_eq!(h1.extensions().get::(), Some(&5)); + h1.extensions_mut().remove::(); + + let mut container = super::Request::try_from(h1).expect("failed converting from http1x"); + container.add_extension(5_u32); + let mut h0 = container + .try_into_http02x() + .expect("failed converting back to http0x"); + assert_eq!(h0.extensions().get::(), Some(&5)); + h0.extensions_mut().remove::(); + req_eq!(h0, req()); + } } diff --git a/rust-runtime/aws-smithy-runtime-api/src/http/response.rs b/rust-runtime/aws-smithy-runtime-api/src/http/response.rs index 84ad832758..efed62da05 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/http/response.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/http/response.rs @@ -55,6 +55,13 @@ impl From for StatusCode { } } +#[cfg(feature = "http-1x")] +impl From for StatusCode { + fn from(value: http1::StatusCode) -> Self { + Self(value.as_u16()) + } +} + impl From for u16 { fn from(value: StatusCode) -> Self { value.0 @@ -73,7 +80,8 @@ pub struct Response { status: StatusCode, headers: Headers, body: B, - extensions: http0::Extensions, + extensions_0x: http0::Extensions, + extensions_1x: http1::Extensions, } impl Response { @@ -90,15 +98,32 @@ impl Response { ) .body(self.body) .expect("known valid"); - let mut headers = http0::HeaderMap::new(); - headers.extend( - self.headers - .headers - .into_iter() - .map(|(k, v)| (k, v.into_http02x())), - ); - *res.headers_mut() = headers; - *res.extensions_mut() = self.extensions; + if self.extensions_0x.len() < self.extensions_1x.len() { + return Err(HttpError::invalid_extensions()); + } + *res.headers_mut() = self.headers.http0_headermap(); + *res.extensions_mut() = self.extensions_0x; + Ok(res) + } + + /// Converts this response into an http 1.x response. + /// + /// Depending on the internal storage type, this operation may be free or it may have an internal + /// cost. + #[cfg(feature = "http-1x")] + pub fn try_into_http1x(self) -> Result, HttpError> { + let mut res = http1::Response::builder() + .status( + http1::StatusCode::from_u16(self.status.into()) + .expect("validated upon construction"), + ) + .body(self.body) + .expect("known valid"); + if self.extensions_1x.len() < self.extensions_0x.len() { + return Err(HttpError::invalid_extensions()); + } + *res.headers_mut() = self.headers.http1_headermap(); + *res.extensions_mut() = self.extensions_1x; Ok(res) } @@ -107,7 +132,8 @@ impl Response { Response { status: self.status, body: f(self.body), - extensions: self.extensions, + extensions_0x: self.extensions_0x, + extensions_1x: self.extensions_1x, headers: self.headers, } } @@ -117,7 +143,8 @@ impl Response { Self { status, body, - extensions: Default::default(), + extensions_0x: Default::default(), + extensions_1x: Default::default(), headers: Default::default(), } } @@ -159,7 +186,8 @@ impl Response { /// Adds an extension to the response extensions pub fn add_extension(&mut self, extension: T) { - self.extensions.insert(extension); + self.extensions_0x.insert(extension.clone()); + self.extensions_1x.insert(extension.clone()); } } @@ -175,37 +203,36 @@ impl TryFrom> for Response { type Error = HttpError; fn try_from(value: http0::Response) -> Result { - use crate::http::headers::HeaderValue; - use http0::HeaderMap; - if let Some(e) = value - .headers() - .values() - .filter_map(|value| std::str::from_utf8(value.as_bytes()).err()) - .next() - { - Err(HttpError::header_was_not_a_string(e)) - } else { - let (parts, body) = value.into_parts(); - let mut string_safe_headers: HeaderMap = Default::default(); - string_safe_headers.extend( - parts - .headers - .into_iter() - .map(|(k, v)| (k, HeaderValue::from_http02x(v).expect("validated above"))), - ); - Ok(Self { - status: StatusCode::try_from(parts.status.as_u16()).expect("validated by http 0.x"), - body, - extensions: parts.extensions, - headers: Headers { - headers: string_safe_headers, - }, - }) - } + let (parts, body) = value.into_parts(); + let headers = Headers::try_from(parts.headers)?; + Ok(Self { + status: StatusCode::try_from(parts.status.as_u16()).expect("validated by http 0.x"), + body, + extensions_0x: parts.extensions, + extensions_1x: http1::Extensions::new(), + headers, + }) + } +} + +#[cfg(feature = "http-1x")] +impl TryFrom> for Response { + type Error = HttpError; + + fn try_from(value: http1::Response) -> Result { + let (parts, body) = value.into_parts(); + let headers = Headers::try_from(parts.headers)?; + Ok(Self { + status: StatusCode::try_from(parts.status.as_u16()).expect("validated by http 0.x"), + body, + extensions_0x: http0::Extensions::new(), + extensions_1x: parts.extensions, + headers, + }) } } -#[cfg(all(test, feature = "http-02x"))] +#[cfg(all(test, feature = "http-02x", feature = "http-1x"))] mod test { use super::*; use aws_smithy_types::body::SdkBody; @@ -229,15 +256,62 @@ mod test { .status(200) .body(SdkBody::from("hello")) .unwrap(); - let mut req = super::Response::try_from(req).unwrap(); - req.headers_mut().insert("a", "b"); - assert_eq!("b", req.headers().get("a").unwrap()); - req.headers_mut().append("a", "c"); - assert_eq!("b", req.headers().get("a").unwrap()); - let http0 = req.try_into_http02x().unwrap(); + let mut rsp = super::Response::try_from(req).unwrap(); + rsp.headers_mut().insert("a", "b"); + assert_eq!("b", rsp.headers().get("a").unwrap()); + rsp.headers_mut().append("a", "c"); + assert_eq!("b", rsp.headers().get("a").unwrap()); + let http0 = rsp.try_into_http02x().unwrap(); assert_eq!(200, http0.status().as_u16()); } + macro_rules! resp_eq { + ($a: expr, $b: expr) => {{ + assert_eq!($a.status(), $b.status(), "status code mismatch"); + assert_eq!($a.headers(), $b.headers(), "header mismatch"); + assert_eq!($a.body().bytes(), $b.body().bytes(), "data mismatch"); + assert_eq!( + $a.extensions().len(), + $b.extensions().len(), + "extensions size mismatch" + ); + }}; + } + + #[track_caller] + fn check_roundtrip(req: impl Fn() -> http0::Response) { + let mut container = super::Response::try_from(req()).unwrap(); + container.add_extension(5_u32); + let mut h1 = container + .try_into_http1x() + .expect("failed converting to http1x"); + assert_eq!(h1.extensions().get::(), Some(&5)); + h1.extensions_mut().remove::(); + + let mut container = super::Response::try_from(h1).expect("failed converting from http1x"); + container.add_extension(5_u32); + let mut h0 = container + .try_into_http02x() + .expect("failed converting back to http0x"); + assert_eq!(h0.extensions().get::(), Some(&5)); + h0.extensions_mut().remove::(); + resp_eq!(h0, req()); + } + + #[test] + fn valid_round_trips() { + let response = || { + http::Response::builder() + .status(200) + .header("k", "v") + .header("multi", "v1") + .header("multi", "v2") + .body(SdkBody::from("12345")) + .unwrap() + }; + check_roundtrip(response); + } + #[test] #[should_panic] fn header_panics() { @@ -252,4 +326,46 @@ mod test { .expect_err("invalid header"); let _ = res.headers_mut().insert("a\nb", "a\nb"); } + + #[test] + fn cant_cross_convert_with_extensions_h0_h1() { + let resp_h0 = || { + http::Response::builder() + .status(200) + .extension(5_u32) + .body(SdkBody::from("hello")) + .unwrap() + }; + + let _ = Response::try_from(resp_h0()) + .unwrap() + .try_into_http1x() + .expect_err("cant copy extension"); + + let _ = Response::try_from(resp_h0()) + .unwrap() + .try_into_http02x() + .expect("allowed to cross-copy"); + } + + #[test] + fn cant_cross_convert_with_extensions_h1_h0() { + let resp_h1 = || { + http1::Response::builder() + .status(200) + .extension(5_u32) + .body(SdkBody::from("hello")) + .unwrap() + }; + + let _ = Response::try_from(resp_h1()) + .unwrap() + .try_into_http02x() + .expect_err("cant copy extension"); + + let _ = Response::try_from(resp_h1()) + .unwrap() + .try_into_http1x() + .expect("allowed to cross-copy"); + } } diff --git a/rust-runtime/aws-smithy-runtime/Cargo.toml b/rust-runtime/aws-smithy-runtime/Cargo.toml index fc08e13ea4..9e77bd8bbe 100644 --- a/rust-runtime/aws-smithy-runtime/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime/Cargo.toml @@ -53,7 +53,8 @@ pretty_assertions = "1.4.0" tokio = { version = "1.25", features = ["macros", "rt", "rt-multi-thread", "test-util", "full"] } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } tracing-test = "0.2.1" -hyper_0_14 = { package = "hyper", version = "0.14.27",features = ["client", "server", "tcp", "http1", "http2"] } +hyper_0_14 = { package = "hyper", version = "0.14.27", features = ["client", "server", "tcp", "http1", "http2"] } +http1 = { package = "http", version = "1" } [package.metadata.docs.rs] all-features = true diff --git a/rust-runtime/aws-smithy-runtime/src/client/http/test_util/replay.rs b/rust-runtime/aws-smithy-runtime/src/client/http/test_util/replay.rs index 2809f428ac..cf638cb682 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/http/test_util/replay.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/http/test_util/replay.rs @@ -260,3 +260,23 @@ impl HttpClient for StaticReplayClient { self.clone().into_shared() } } + +#[cfg(test)] +mod test { + use crate::client::http::test_util::{ReplayEvent, StaticReplayClient}; + use aws_smithy_types::body::SdkBody; + + #[test] + fn create_from_either_http_type() { + let client = StaticReplayClient::new(vec![ReplayEvent::new( + http1::Request::builder() + .uri("test") + .body(SdkBody::from("hello")) + .unwrap(), + http1::Response::builder() + .status(200) + .body(SdkBody::from("hello")) + .unwrap(), + )]); + } +}