This commit is contained in:
Russell Cohen 2023-08-29 12:29:49 -04:00
parent 04354ad3b0
commit 2b0920a8d5
15 changed files with 199 additions and 138 deletions

View File

@ -358,15 +358,9 @@ impl Signer for SigV4Signer {
});
let signable_request = SignableRequest::new(
request.method().as_str(),
request.method(),
request.uri().to_string(),
request.headers().iter().map(|(k, v)| {
(
k.as_str(),
// use from_utf8 instead of to_str because we _do_ allow non-ascii header values
std::str::from_utf8(v.as_bytes()).expect("only utf-8 headers are signable"),
)
}),
request.headers().iter(),
signable_body,
)?;
sign(signable_request, &signing_params)?
@ -392,7 +386,7 @@ impl Signer for SigV4Signer {
.expect("failed to send deferred signer");
}
}
signing_instructions.apply_to_request(request);
// signing_instructions.apply_to_request(request);
Ok(())
}
}

View File

@ -4,6 +4,7 @@
*/
use crate::query::fmt_string as percent_encode_query;
use http::uri::InvalidUri;
use http::Uri;
/// Utility for updating the query string in a [`Uri`].
@ -15,6 +16,10 @@ pub struct QueryWriter {
}
impl QueryWriter {
pub fn new_from_string(uri: &str) -> Result<Self, InvalidUri> {
Ok(Self::new(&Uri::try_from(uri)?))
}
/// Creates a new `QueryWriter` based off the given `uri`.
pub fn new(uri: &Uri) -> Self {
let new_path_and_query = uri

View File

@ -4,7 +4,9 @@
*/
use aws_smithy_http::body::SdkBody;
use aws_smithy_http::endpoint::error::InvalidEndpointError;
use http as http0;
use http::uri::PathAndQuery;
use http0::header::Iter;
use http0::{Extensions, HeaderMap, Method};
use std::borrow::Cow;
@ -28,6 +30,55 @@ pub struct Uri {
parsed: http0::Uri,
}
impl Uri {
/// Sets `endpoint` as the endpoint for a URL.
///
/// An `endpoint` MUST contain a scheme and authority.
/// An `endpoint` MAY contain a port and path.
///
/// An `endpoint` MUST NOT contain a query
pub fn set_endpoint(&mut self, endpoint: &str) -> Result<(), HttpError> {
let endpoint: http0::Uri = endpoint.parse().map_err(HttpError::invalid_uri)?;
let endpoint = endpoint.into_parts();
let authority = endpoint
.authority
.ok_or_else(|| HttpError::new("endpoint must contain authority"))?;
let scheme = endpoint
.scheme
.ok_or_else(|| HttpError::new("endpoint must have scheme"))?;
let new_uri = http0::Uri::builder()
.authority(authority)
.scheme(scheme.clone())
.path_and_query(merge_paths(endpoint.path_and_query, &self.parsed).as_ref())
.build()
.map_err(HttpError::new)?;
self.as_string = new_uri.to_string();
self.parsed = new_uri;
Ok(())
}
}
fn merge_paths<'a>(endpoint_path: Option<PathAndQuery>, uri: &'a http0::Uri) -> Cow<'a, str> {
let uri_path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("");
let endpoint_path = match endpoint_path {
None => return Cow::Borrowed(uri_path_and_query),
Some(path) => path,
};
if let Some(query) = endpoint_path.query() {
tracing::warn!(query = %query, "query specified in endpoint will be ignored during endpoint resolution");
}
let endpoint_path = endpoint_path.path();
if endpoint_path.is_empty() {
Cow::Borrowed(uri_path_and_query)
} else {
let ep_no_slash = endpoint_path.strip_suffix('/').unwrap_or(endpoint_path);
let uri_path_no_slash = uri_path_and_query
.strip_prefix('/')
.unwrap_or(uri_path_and_query);
Cow::Owned(format!("{}/{}", ep_no_slash, uri_path_no_slash))
}
}
impl TryFrom<String> for Uri {
type Error = HttpError;
@ -121,6 +172,11 @@ impl<B> Request<B> {
&self.uri.as_string
}
/// Returns a mutable reference the the URI of this http::Request
pub fn uri_mut(&mut self) -> &mut Uri {
&mut self.uri
}
/// Sets the URI of this request
pub fn set_uri<U>(&mut self, uri: U) -> Result<(), U::Error>
where
@ -230,6 +286,13 @@ impl Headers {
self.headers.get(key.as_ref()).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()
@ -291,6 +354,13 @@ impl Headers {
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: &str) -> Option<HeaderValue> {
self.headers.remove(key)
}
/// Appends a value to a given key
///
/// # Panics
@ -457,7 +527,9 @@ fn header_value(value: MaybeStatic) -> Result<HeaderValue, HttpError> {
#[cfg(test)]
mod test {
use http::HeaderValue;
use aws_smithy_http::body::SdkBody;
use http::header::{AUTHORIZATION, CONTENT_LENGTH};
use http::{HeaderValue, Uri};
#[test]
fn headers_can_be_any_string() {
@ -467,4 +539,24 @@ mod test {
.parse::<HeaderValue>()
.expect_err("cannot contain control characters");
}
#[test]
fn try_clone_clones_all_data() {
let request = ::http::Request::builder()
.uri(Uri::from_static("https://www.amazon.com"))
.method("POST")
.header(CONTENT_LENGTH, 456)
.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");
assert_eq!("https://www.amazon.com/", cloned.uri());
assert_eq!("POST", cloned.method());
assert_eq!(2, cloned.headers().len());
assert_eq!("Token: hello", cloned.headers().get(AUTHORIZATION).unwrap(),);
assert_eq!("456", cloned.headers().get(CONTENT_LENGTH).unwrap());
assert_eq!("hello world!".as_bytes(), cloned.body().bytes().unwrap());
}
}

View File

@ -166,6 +166,14 @@ impl<I, O, E> InterceptorContext<I, O, E> {
self.request = Some(request);
}
#[cfg(test)]
pub(crate) fn set_http03_request(
&mut self,
request: http::Request<aws_smithy_http::body::SdkBody>,
) {
self.request = Some(request.try_into().expect("request could not be converted"));
}
/// Retrieve the transmittable request for the operation being invoked.
/// This will only be available once request marshalling has completed.
pub fn request(&self) -> Option<&Request> {
@ -429,26 +437,14 @@ impl fmt::Display for RewindResult {
}
fn try_clone(request: &HttpRequest) -> Option<HttpRequest> {
let cloned_body = request.body().try_clone()?;
let mut cloned_request = ::http::Request::builder()
.uri(request.uri().clone())
.method(request.method());
*cloned_request
.headers_mut()
.expect("builder has not been modified, headers must be valid") = request.headers().clone();
Some(
cloned_request
.body(cloned_body)
.expect("a clone of a valid request should be a valid request"),
)
request.try_clone()
}
#[cfg(all(test, feature = "test-util"))]
mod tests {
use super::*;
use aws_smithy_http::body::SdkBody;
use http::header::{AUTHORIZATION, CONTENT_LENGTH};
use http::{HeaderValue, Uri};
use http::HeaderValue;
#[test]
fn test_success_transitions() {
@ -461,7 +457,7 @@ mod tests {
context.enter_serialization_phase();
let _ = context.take_input();
context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap());
context.set_request(HttpRequest::new(SdkBody::empty()));
context.enter_before_transmit_phase();
context.request();
@ -502,7 +498,7 @@ mod tests {
context.enter_serialization_phase();
let _ = context.take_input();
context.set_request(
context.set_http03_request(
http::Request::builder()
.header("test", "the-original-un-mutated-request")
.body(SdkBody::empty())
@ -512,7 +508,6 @@ mod tests {
context.save_checkpoint();
assert_eq!(context.rewind(&mut cfg), RewindResult::Unnecessary);
// Modify the test header post-checkpoint to simulate modifying the request for signing or a mutating interceptor
context.request_mut().unwrap().headers_mut().remove("test");
context.request_mut().unwrap().headers_mut().insert(
"test",
HeaderValue::from_static("request-modified-after-signing"),
@ -551,23 +546,4 @@ mod tests {
let output = context.output_or_error.unwrap().expect("success");
assert_eq!("output", output.downcast_ref::<String>().unwrap());
}
#[test]
fn try_clone_clones_all_data() {
let request = ::http::Request::builder()
.uri(Uri::from_static("https://www.amazon.com"))
.method("POST")
.header(CONTENT_LENGTH, 456)
.header(AUTHORIZATION, "Token: hello")
.body(SdkBody::from("hello world!"))
.expect("valid request");
let cloned = try_clone(&request).expect("request is cloneable");
assert_eq!(&Uri::from_static("https://www.amazon.com"), cloned.uri());
assert_eq!("POST", cloned.method());
assert_eq!(2, cloned.headers().len());
assert_eq!("Token: hello", cloned.headers().get(AUTHORIZATION).unwrap(),);
assert_eq!("456", cloned.headers().get(CONTENT_LENGTH).unwrap());
assert_eq!("hello world!".as_bytes(), cloned.body().bytes().unwrap());
}
}

View File

@ -30,7 +30,7 @@ use std::future::Future as StdFuture;
use std::pin::Pin;
/// Type alias for the HTTP request type that the orchestrator uses.
pub type HttpRequest = http::Request<SdkBody>;
pub type HttpRequest = crate::client::http::Request<SdkBody>;
/// Type alias for the HTTP response type that the orchestrator uses.
pub type HttpResponse = http::Response<SdkBody>;

View File

@ -419,13 +419,7 @@ mod tests {
let resp = components
.http_connector()
.unwrap()
.call(
http::Request::builder()
.method("GET")
.uri("/")
.body(SdkBody::empty())
.unwrap(),
)
.call(HttpRequest::new(SdkBody::empty()))
.await
.unwrap();
dbg!(&resp);

View File

@ -20,8 +20,6 @@ use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
use aws_smithy_runtime_api::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
use aws_smithy_types::base64::encode;
use aws_smithy_types::config_bag::ConfigBag;
use http::header::HeaderName;
use http::HeaderValue;
/// Destination for the API key
#[derive(Copy, Clone, Debug)]
@ -93,17 +91,20 @@ impl Signer for ApiKeySigner {
.ok_or("HTTP ApiKey auth requires a `Token` identity")?;
match self.location {
ApiKeyLocation::Header => {
request.headers_mut().append(
HeaderName::try_from(&self.name).expect("valid API key header name"),
HeaderValue::try_from(format!("{} {}", self.scheme, api_key.token())).map_err(
|_| "API key contains characters that can't be included in a HTTP header",
)?,
);
request
.headers_mut()
.try_append(
self.name.clone(),
format!("{} {}", self.scheme, api_key.token(),),
)
.map_err(|_| {
"API key contains characters that can't be included in a HTTP header"
})?;
}
ApiKeyLocation::Query => {
let mut query = QueryWriter::new(request.uri());
let mut query = QueryWriter::new_from_string(request.uri())?;
query.insert(&self.name, api_key.token());
*request.uri_mut() = query.build_uri();
request.set_uri(query.build_uri()).expect("infallible");
}
}
@ -159,12 +160,11 @@ impl Signer for BasicAuthSigner {
.data::<Login>()
.ok_or("HTTP basic auth requires a `Login` identity")?;
request.headers_mut().insert(
http::header::AUTHORIZATION,
HeaderValue::from_str(&format!(
http::header::AUTHORIZATION.as_str(),
format!(
"Basic {}",
encode(format!("{}:{}", login.user(), login.password()))
))
.expect("valid header value"),
),
);
Ok(())
}
@ -217,12 +217,15 @@ impl Signer for BearerAuthSigner {
let token = identity
.data::<Token>()
.ok_or("HTTP bearer auth requires a `Token` identity")?;
request.headers_mut().insert(
http::header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", token.token())).map_err(|_| {
request
.headers_mut()
.try_insert(
http::header::AUTHORIZATION.as_str(),
format!("Bearer {}", token.token()),
)
.map_err(|_| {
"Bearer token contains characters that can't be included in a HTTP header"
})?,
);
})?;
Ok(())
}
}
@ -297,6 +300,8 @@ mod tests {
let mut request = http::Request::builder()
.uri("http://example.com/Foobaz")
.body(SdkBody::empty())
.unwrap()
.try_into()
.unwrap();
signer
.sign_http_request(
@ -327,6 +332,8 @@ mod tests {
let mut request = http::Request::builder()
.uri("http://example.com/Foobaz")
.body(SdkBody::empty())
.unwrap()
.try_into()
.unwrap();
signer
.sign_http_request(
@ -350,7 +357,11 @@ mod tests {
let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
let config_bag = ConfigBag::base();
let identity = Identity::new(Login::new("Aladdin", "open sesame", None), None);
let mut request = http::Request::builder().body(SdkBody::empty()).unwrap();
let mut request = http::Request::builder()
.body(SdkBody::empty())
.unwrap()
.try_into()
.unwrap();
signer
.sign_http_request(
@ -374,7 +385,11 @@ mod tests {
let config_bag = ConfigBag::base();
let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
let identity = Identity::new(Token::new("some-token", None), None);
let mut request = http::Request::builder().body(SdkBody::empty()).unwrap();
let mut request = http::Request::builder()
.body(SdkBody::empty())
.unwrap()
.try_into()
.unwrap();
signer
.sign_http_request(
&mut request,

View File

@ -17,6 +17,7 @@ pub mod adapter {
use aws_smithy_client::erase::DynConnector;
use aws_smithy_runtime_api::client::connectors::HttpConnector;
use aws_smithy_runtime_api::client::orchestrator::{BoxFuture, HttpRequest, HttpResponse};
use std::future::ready;
use std::sync::{Arc, Mutex};
/// Adapts a [`DynConnector`] to the [`HttpConnector`] trait.
@ -41,7 +42,11 @@ pub mod adapter {
impl HttpConnector for DynConnectorAdapter {
fn call(&self, request: HttpRequest) -> BoxFuture<HttpResponse> {
let future = self.dyn_connector.lock().unwrap().call_lite(request);
let req = match request.into_http03x() {
Err(e) => return Box::pin(ready(Err(Box::new(e) as _))),
Ok(req) => req,
};
let future = self.dyn_connector.lock().unwrap().call_lite(req);
future
}
}

View File

@ -54,8 +54,7 @@ impl Interceptor for ConnectionPoisoningInterceptor {
let capture_smithy_connection = CaptureSmithyConnectionWrapper::new();
context
.request_mut()
.extensions_mut()
.insert(capture_smithy_connection.clone_inner());
.add_extension(capture_smithy_connection.clone_inner());
cfg.interceptor_state().store_put(capture_smithy_connection);
Ok(())

View File

@ -11,7 +11,7 @@ use aws_smithy_http::result::ConnectorError;
use aws_smithy_protocol_test::{assert_ok, validate_body, MediaType};
use aws_smithy_runtime_api::client::connectors::HttpConnector;
use aws_smithy_runtime_api::client::orchestrator::{BoxFuture, HttpRequest, HttpResponse};
use http::header::{HeaderName, CONTENT_TYPE};
use http::header::CONTENT_TYPE;
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::{Arc, Mutex};
@ -146,7 +146,7 @@ struct ValidateRequest {
}
impl ValidateRequest {
fn assert_matches(&self, index: usize, ignore_headers: &[HeaderName]) {
fn assert_matches(&self, index: usize, ignore_headers: &[&str]) {
let (actual, expected) = (&self.actual, &self.expected);
assert_eq!(
actual.uri(),
@ -154,14 +154,13 @@ impl ValidateRequest {
"Request #{index} - URI doesn't match expected value"
);
for (name, value) in expected.headers() {
if !ignore_headers.contains(name) {
if !ignore_headers.contains(&name) {
let actual_header = actual
.headers()
.get(name)
.unwrap_or_else(|| panic!("Request #{index} - Header {name:?} is missing"));
assert_eq!(
actual_header.to_str().unwrap(),
value.to_str().unwrap(),
actual_header, value,
"Request #{index} - Header {name:?} doesn't match expected value",
);
}
@ -171,7 +170,7 @@ impl ValidateRequest {
let media_type = if actual
.headers()
.get(CONTENT_TYPE)
.map(|v| v.to_str().unwrap().contains("json"))
.map(|v| v.contains("json"))
.unwrap_or(false)
{
MediaType::Json
@ -225,7 +224,7 @@ impl TestConnector {
/// A list of headers that should be ignored when comparing requests can be passed
/// for cases where headers are non-deterministic or are irrelevant to the test.
#[track_caller]
pub fn assert_requests_match(&self, ignore_headers: &[HeaderName]) {
pub fn assert_requests_match(&self, ignore_headers: &[&str]) {
for (i, req) in self.requests().iter().enumerate() {
req.assert_matches(i, ignore_headers)
}

View File

@ -437,7 +437,7 @@ mod tests {
use crate::client::test_util::{
deserializer::CannedResponseDeserializer, serializer::CannedRequestSerializer,
};
use ::http::{Request, Response, StatusCode};
use ::http::{Response, StatusCode};
use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
use aws_smithy_runtime_api::client::auth::{
AuthSchemeOptionResolverParams, SharedAuthSchemeOptionResolver,
@ -465,11 +465,7 @@ mod tests {
use tracing_test::traced_test;
fn new_request_serializer() -> CannedRequestSerializer {
CannedRequestSerializer::success(
Request::builder()
.body(SdkBody::empty())
.expect("request is valid"),
)
CannedRequestSerializer::success(HttpRequest::new(SdkBody::empty()))
}
fn new_response_deserializer() -> CannedResponseDeserializer {

View File

@ -3,7 +3,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
use crate::client::auth::no_auth::NO_AUTH_SCHEME_ID;
use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_runtime_api::client::auth::{
AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOptionResolver,
@ -125,15 +124,10 @@ fn extract_endpoint_auth_scheme_config(
endpoint: &Endpoint,
scheme_id: AuthSchemeId,
) -> Result<AuthSchemeEndpointConfig<'_>, AuthOrchestrationError> {
// TODO(P96049742): Endpoint config doesn't currently have a concept of optional auth or "no auth", so
// we are short-circuiting lookup of endpoint auth scheme config if that is the selected scheme.
if scheme_id == NO_AUTH_SCHEME_ID {
return Ok(AuthSchemeEndpointConfig::empty());
}
let auth_schemes = match endpoint.properties().get("authSchemes") {
Some(Document::Array(schemes)) => schemes,
// no auth schemes:
None => return Ok(AuthSchemeEndpointConfig::empty()),
None => return Ok(AuthSchemeEndpointConfig::from(None)),
_other => {
return Err(AuthOrchestrationError::BadAuthSchemeEndpointConfig(
"expected an array for `authSchemes` in endpoint config".into(),
@ -199,7 +193,7 @@ mod tests {
) -> Result<(), BoxError> {
request
.headers_mut()
.insert(http::header::AUTHORIZATION, "success!".parse().unwrap());
.insert(http::header::AUTHORIZATION.as_str(), "success!");
Ok(())
}
}
@ -229,7 +223,7 @@ mod tests {
let mut ctx = InterceptorContext::new(Input::doesnt_matter());
ctx.enter_serialization_phase();
ctx.set_request(http::Request::builder().body(SdkBody::empty()).unwrap());
ctx.set_request(HttpRequest::new(SdkBody::empty()));
let _ = ctx.take_input();
ctx.enter_before_transmit_phase();
@ -275,7 +269,7 @@ mod tests {
let mut ctx = InterceptorContext::new(Input::doesnt_matter());
ctx.enter_serialization_phase();
ctx.set_request(http::Request::builder().body(SdkBody::empty()).unwrap());
ctx.set_request(HttpRequest::new(SdkBody::empty()));
let _ = ctx.take_input();
ctx.enter_before_transmit_phase();
@ -324,7 +318,7 @@ mod tests {
config_with_identity(HTTP_BEARER_AUTH_SCHEME_ID, Token::new("t", None));
let mut ctx = InterceptorContext::new(Input::erase("doesnt-matter"));
ctx.enter_serialization_phase();
ctx.set_request(http::Request::builder().body(SdkBody::empty()).unwrap());
ctx.set_request(HttpRequest::new(SdkBody::empty()));
let _ = ctx.take_input();
ctx.enter_before_transmit_phase();
orchestrate_auth(&mut ctx, &runtime_components, &cfg)

View File

@ -4,10 +4,7 @@
*/
use aws_smithy_http::endpoint::error::ResolveEndpointError;
use aws_smithy_http::endpoint::{
apply_endpoint as apply_endpoint_to_request_uri, EndpointPrefix, ResolveEndpoint,
SharedEndpointResolver,
};
use aws_smithy_http::endpoint::{EndpointPrefix, ResolveEndpoint, SharedEndpointResolver};
use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_runtime_api::client::endpoint::{EndpointResolver, EndpointResolverParams};
use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
@ -15,8 +12,8 @@ use aws_smithy_runtime_api::client::orchestrator::{Future, HttpRequest};
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
use aws_smithy_types::endpoint::Endpoint;
use http::header::HeaderName;
use http::{HeaderValue, Uri};
use http::Uri;
use std::borrow::Cow;
use std::fmt::Debug;
use std::str::FromStr;
use tracing::trace;
@ -121,7 +118,6 @@ pub(super) async fn orchestrate_endpoint(
.load::<EndpointResolverParams>()
.expect("endpoint resolver params must be set");
let endpoint_prefix = cfg.load::<EndpointPrefix>();
tracing::debug!(endpoint_params = ?params, endpoint_prefix = ?endpoint_prefix, "resolving endpoint");
let request = ctx.request_mut().expect("set during serialization");
let endpoint = runtime_components
@ -141,31 +137,31 @@ fn apply_endpoint(
endpoint: &Endpoint,
endpoint_prefix: Option<&EndpointPrefix>,
) -> Result<(), BoxError> {
let uri: Uri = endpoint.url().parse().map_err(|err| {
ResolveEndpointError::from_source("endpoint did not have a valid uri", err)
})?;
apply_endpoint_to_request_uri(request.uri_mut(), &uri, endpoint_prefix).map_err(|err| {
let prefixed_endpoint = match endpoint_prefix {
Some(prefix) => Cow::Owned(format!("{}{}", prefix.as_str(), endpoint.url())),
None => Cow::Borrowed(endpoint.url()),
};
request
.uri_mut()
.set_endpoint(prefixed_endpoint.as_ref())
.map_err(|err| {
ResolveEndpointError::message(format!(
"failed to apply endpoint `{:?}` to request `{:?}`",
uri, request,
endpoint.url(),
request,
))
.with_source(Some(err.into()))
})?;
for (header_name, header_values) in endpoint.headers() {
request.headers_mut().remove(header_name);
for value in header_values {
request.headers_mut().insert(
HeaderName::from_str(header_name).map_err(|err| {
ResolveEndpointError::message("invalid header name")
request
.headers_mut()
.try_insert(header_name.to_string(), value.to_string())
.map_err(|err| {
ResolveEndpointError::message("invalid header key or value value")
.with_source(Some(err.into()))
})?,
HeaderValue::from_str(value).map_err(|err| {
ResolveEndpointError::message("invalid header value")
.with_source(Some(err.into()))
})?,
);
})?;
}
}
Ok(())

View File

@ -25,6 +25,5 @@ pub mod client;
/// A data structure for persisting and sharing state between multiple clients.
pub mod static_partition_map;
/// General testing utilities.
#[cfg(feature = "test-util")]
/// Test utilities
pub mod test_util;

View File

@ -58,12 +58,9 @@ impl Interceptor for HttpChecksumRequiredInterceptor {
.bytes()
.expect("checksum can only be computed for non-streaming operations");
let checksum = <md5::Md5 as md5::Digest>::digest(body_bytes);
request.headers_mut().insert(
HeaderName::from_static("content-md5"),
base64::encode(&checksum[..])
.parse()
.expect("checksum is a valid header value"),
);
request
.headers_mut()
.insert("content-md5", base64::encode(&checksum[..]));
Ok(())
}
}