Skip to content

Commit

Permalink
Add http = "1.0" support to the http request wrapper (#3373)
Browse files Browse the repository at this point in the history
## Motivation and Context
- aws-sdk-rust#977
- smithy-rs#3365

## Description
Add `try_from` and `try_into` for HTTP 1.x to the HTTP request/response
wrapper. This is a stepping stone en route to supporting Hyper 1.0

## Testing
- [x] New unit tests

## Checklist
<!--- If a checkbox below is not applicable, then please DELETE it
rather than leaving it unchecked -->
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the
smithy-rs codegen or runtime crates
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the AWS
SDK, generated SDK code, or SDK runtime crates

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._

---------

Co-authored-by: John DiSanti <[email protected]>
  • Loading branch information
rcoh and jdisanti authored Jan 25, 2024
1 parent e239b46 commit 8873666
Show file tree
Hide file tree
Showing 12 changed files with 514 additions and 84 deletions.
20 changes: 19 additions & 1 deletion CHANGELOG.next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,22 @@
# message = "Fix typos in module documentation for generated crates"
# references = ["smithy-rs#920"]
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
# author = "rcoh"
# author = "rcoh"

[[aws-sdk-rust]]
message = "The types in the aws-http crate were moved into aws-runtime. Deprecated type aliases were put in place to point to the new locations."
references = ["smithy-rs#3355"]
meta = { "breaking" = false, "tada" = false, "bug" = false }
author = "jdisanti"

[[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"
2 changes: 1 addition & 1 deletion aws/rust-runtime/aws-config/src/test_case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ impl TestEnvironment {
.await
{
Ok(()) => {}
Err(e) => panic!("{}", e),
Err(e) => panic!("{}", DisplayErrorContext(e.as_ref())),
}
let contents = rx.contents();
let leaking_lines = self.lines_with_secrets(&contents);
Expand Down
4 changes: 3 additions & 1 deletion rust-runtime/aws-smithy-runtime-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions rust-runtime/aws-smithy-runtime-api/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! HTTP request and response types
mod error;
mod extensions;
mod headers;
mod request;
mod response;
Expand Down
5 changes: 5 additions & 0 deletions rust-runtime/aws-smithy-runtime-api/src/http/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
67 changes: 67 additions & 0 deletions rust-runtime/aws-smithy-runtime-api/src/http/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

use crate::http::HttpError;
use http as http0;

#[derive(Default, Debug)]
pub(crate) struct Extensions {
extensions_02x: http0::Extensions,
extensions_1x: http1::Extensions,
}

impl Extensions {
pub(crate) fn new() -> Self {
Self::default()
}

/// Adds an extension to the request extensions
pub(crate) fn insert<T: Send + Sync + Clone + 'static>(&mut self, extension: T) {
self.extensions_1x.insert(extension.clone());
self.extensions_02x.insert(extension);
}
}

impl From<http0::Extensions> for Extensions {
fn from(value: http0::Extensions) -> Self {
Self {
extensions_02x: value,
extensions_1x: Default::default(),
}
}
}

impl From<http1::Extensions> for Extensions {
fn from(value: http1::Extensions) -> Self {
Self {
extensions_02x: Default::default(),
extensions_1x: value,
}
}
}

impl TryFrom<Extensions> for http0::Extensions {
type Error = HttpError;

fn try_from(value: Extensions) -> Result<Self, Self::Error> {
if value.extensions_1x.len() > value.extensions_02x.len() {
Err(HttpError::invalid_extensions())
} else {
Ok(value.extensions_02x)
}
}
}

impl TryFrom<Extensions> for http1::Extensions {
type Error = HttpError;

fn try_from(value: Extensions) -> Result<Self, Self::Error> {
if value.extensions_02x.len() > value.extensions_1x.len() {
Err(HttpError::invalid_extensions())
} else {
Ok(value.extensions_1x)
}
}
}
93 changes: 88 additions & 5 deletions rust-runtime/aws-smithy-runtime-api/src/http/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -181,6 +204,34 @@ impl TryFrom<HeaderMap> for Headers {
}
}

#[cfg(feature = "http-1x")]
impl TryFrom<http1::HeaderMap> for Headers {
type Error = HttpError;

fn try_from(value: http1::HeaderMap) -> Result<Self, Self::Error> {
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<HeaderValue> = 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 {
Expand Down Expand Up @@ -273,25 +324,57 @@ 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 {
#[allow(dead_code)]
pub(crate) fn from_http02x(value: http0::HeaderValue) -> Result<Self, Utf8Error> {
let _ = std::str::from_utf8(value.as_bytes())?;
Ok(Self { _private: value })
Ok(Self {
_private: Inner::H0(value),
})
}

#[allow(dead_code)]
pub(crate) fn from_http1x(value: http1::HeaderValue) -> Result<Self, Utf8Error> {
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<str> 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")
}
}

Expand Down
Loading

0 comments on commit 8873666

Please sign in to comment.