RFC: Eliminating Public http
dependencies
Status: Accepted
Applies to: client
For a summarized list of proposed changes, see the Changes Checklist section.
This RFC defines how we plan to refactor the SDK to allow the SDK to consume a 1.0
version of hyper
, http-body
,
and http
at a later date. Currently, hyper
is 0.14.x
and a 1.0
release candidate series is in progress. However,
there are open questions that may significantly delay the launch
of
these three crates. We do not want to tie the 1.0
of the Rust SDK to these crates.
Terminology
- http-body: A crate (and trait) defining how HTTP bodies work. Notably, the change from
0.*
to1.0
changeshttp-body
to operate on frames instead of having separate methods. http
(crate): a low level crate ofhttp
primitives (no logic, just requests and responses)- ossified dependency: An ossified dependency describes a dependency that, when a new version is released, cannot be utilized without breaking changes. For example, if the
mutate_request
function on every operation operates on&mut http::Request
wherehttp = 0.2
, that dependency is "ossified." Compare this to a function that offers the ability to convert something into anhttp = 0.2
request—since http=1 and http=0.2 are largely equivalent, the existence of this function does not prevent us from using http = 1 in the future. In general terms, functions that operate on references are much more likely to ossify—There is no practical way for someone to mutate anhttp = 0.2
request if you have anhttp = 1
request other than a time-consuming clone, and reconversion process.
Why is this important?
Performance:
At some point in the Future, hyper = 1
, http = 1
and http-body = 1
will be released. It takes ~1-2 microseconds to rebuild an HTTP request. If we assume that hyper = 1
will only operate on http = 1
requests, then if we can't use http = 1
requests internally, our only way of supporting hyper = 1
will be to convert the HTTP request at dispatch time. Besides pinning us to a potentially unsupported version of the HTTP crate, this will prevent us from directly dispatching requests in an efficient manner. With a total overhead of 20µs for the SDK, 1µs is not insignificant. Furthermore, it grows as the number of request headers grow. A benchmark should be run for a realistic HTTP request e.g. one that we send to S3.
Hyper Upgrade: Hyper 1 is significantly more flexible than Hyper 0.14.x, especially WRT to connection management & pooling. If we don't make these changes, the upgrade to Hyper 1.x could be significantly more challenging.
Security Fixes:
If we're still on http = 0.*
and a vulnerability is identified, we may end up needing to manually contribute the patch. The http
crate is not trivial and contains parsing logic and optimized code (including a non-trivial amount of unsafe
). See this GitHub issue. Notable is that one issue may be unsound and result in changing the public API.
API Friendliness
If we ship with an API that public exposes customers to http = 0.*
, we have the API forever. We have to consider that we aren't shipping the Rust SDK for this month or even this year but probably the Rust SDK for the next 5-10 years.
Future CRT Usage If we make this change, we enable a future where we can use the CRT HTTP request type natively without needing a last minute conversion to the CRT HTTP Request type.
struct HttpRequest {
inner: Inner
}
enum Inner {
Httpv0(http_0::Request),
Httpv1(http_1::Request),
Crt(aws_crt_http::Request)
}
The user experience if this RFC is implemented
Customers are impacted in 3 main locations:
- HTTP types in Interceptors
- HTTP types in
customize(...)
- HTTP types in Connectors
In all three of these cases, users would interact with our http
wrapper types instead.
In the current version of the SDK, we expose public dependencies on the http
crate in several key places:
- The
sigv4
crate. Thesigv4
crate currently operates directly on many types from thehttp
crate. This is unnecessary and actually makes the crate more difficult to use. Althoughhttp
may be used internally,http
will be removed from the public API of this crate. - Interceptor Context:
interceptor
s can mutate the HTTP request through an unshielded interface. This requires creating a wrapper layer aroundhttp::Request
and updating already written interceptors. aws-config
:http::Response
anduri
- A long tail of exposed requests and responses in the runtime crates. Many of these crates will be removed post-orchestrator so this can be temporarily delayed.
How to actually implement this RFC
Enabling API evolution
One key mechanism that we SHOULD use for allowing our APIs to evolve in the future is usage of ~
version bounds for the runtime crates after releasing 1.0.
Http Request Wrapper
In order to enable HTTP evolution, we will create a set of wrapper structures around http::Request
and http::Response
. These will use http = 0
internally. Since the HTTP crate itself is quite small, including private dependencies on both versions of the crate is a workable solution. In general, we will aim for an API that is close to drop-in compatible to the HTTP crate while ensuring that a different crate could be used as the backing storage.
// since it's our type, we can default `SdkBody`
pub struct Request<B = SdkBody> {
// this uses the http = 0.2 request. In the future, we can make an internal enum to allow storing an http = 1
http_0: http::Request<B>
}
Conversion to/from http::Request
One key property here is that although converting to/from an http::Request
can be expensive, this is not ossification of the API. This is because the API can support converting from/to both http = 0
and http = 1
in the future—because it offers mutation of the request via a unified interface, the request would only need to be converted once for dispatch if there was a mismatch (instead of repeatedly). At some point in the future, the http = 0
representation could be deprecated and removed or feature gated.
Challenges
- Creating an HTTP API which is forwards compatible, idiomatic and "truthful" without relying on existing types from Hyper—e.g. when adding a header, we need to account for the possibility that a header is invalid.
- Allow for future forwards-compatible evolution in the API—A lot of thought went into the
http
crate API w.r.t method parameters, types, and generics. Although we can aim for a simpler solution in some cases (e.g. accepting&str
instead ofHeaderName
), we need to be careful that we do so while allowing API evolution.
Removing the SigV4 HTTP dependency
The SigV4 crate signs a number of HTTP
types directly. We should change it to accept strings, and when appropriate, iterators of strings for headers.
Removing the HTTP dependency from generated clients
Generated clients currently include a public HTTP dependency in customize
. This should be changed to accept our HTTP
wrapper type instead or be restricted to a subset of operations (e.g. add_header
) while forcing users to add an interceptor if they need full control.
Changes checklist
-
Create the
http::Request
wrapper. Carefully audit for compatibility without breaking changes. 5 Days. - Refactor currently written interceptors to use the wrapper: 2 days.
- Refactor the SigV4 crate to remove the HTTP dependency from the public interface: 2 days.
-
Add / validate support for SdkBody
http-body = 1.0rc.2
either in a PR or behind a feature gate. Test this to ensure it works with Hyper. Some previous work here exists: 1 week -
Remove
http::Response
andUri
from the public exposed types inaws-config
: 1-4 days. - Long tail of other usages: 1 week
-
Implement
~
versions for SDK Crate => runtime crate dependencies: 1 week