Align current hyper to the hyper VISION.
The VISION outlines a decision-making framework, use-cases, and general shape of hyper. This roadmap describes the currently known problems with hyper, and then shows what changes are needed to make hyper 1.0 look more like what is in the VISION.
Note: These known issues are as of hyper v0.14.x. After v1.0 is released, ideally these issues will have been solved. Keeping this history may be helpful to Future Us, though.
Both the higher-level Client
and Server
types have stability concerns.
For the hyper::Server
:
- The
Accept
trait is complex, and too easy to get wrong. If used with TLS, a slow TLS handshake can affect all other new connections waiting for it to finish. - The
MakeService<&IO>
is confusing. The bounds are an assault on the eyes. - The
MakeService
API doesn't allow to easily annotate the HTTP connection withtracing
. - Graceful shutdown doesn't give enough control.
It's more common for people to simply use hyper::server::conn
at this point,
than to bother with the hyper::Server
.
While the hyper::Client
is much easier to use, problems still exist:
- The whole
Connect
design isn't stable.- ALPN and proxies can provide surprising extra configuration of connections.
- Some
Connect
implementations may wish to view the path, in addition to the scheme, host, and port. - Wants
runtime
feature
- The Pool could be made more general or composable. At the same time, more customization is desired, and it's not clear how to expose it yet.
hyper has been able to support different runtimes, but it has sometimes awkward default support for Tokio.
- The
runtime
cargo-feature isn't additive - Built-in Tokio support can be confusing
- Executors and Timers
- The
runtime
feature currently enables a few options that require a timer, such as timeouts and keepalive intervals. It implicitly relies on Tokio's timer context. This can be quite confusing.
- The
- IO traits
- Should we publicly depend on Tokio's traits?
futures-io
?- Definitely nope.
- Not stable. (0.3?)
- No uninitialized memory.
- Eventual
std
traits?- They've been in design for years.
- We cannot base our schedule on them.
- When they are stable, we can:
- Provide a bridge in
hyper-util
. - Consider a 2.0 of hyper.
- Provide a bridge in
- Define our own traits, provide util wrappers?
There's a concern about forwards-compatibility. We want to be able to add
support for new HTTP features without needing a new major version. While most
of http
and hyper
are prepared for that, there's two potential problems.
- New frames on an HTTP stream (body)
- Receiving a new frame type would require a new trait method
- There's no way to implement a "receive unknown frame" that hyper doesn't know about.
- Sending an unknown frame type would be even harder.
- Besides being able to pass an "unknown" type through the trait, the user would need to be able to describe how that frame is encoded in HTTP/2/3.
- Receiving a new frame type would require a new trait method
- New HTTP versions
- HTTP/3 will require a new transport abstraction. It's not as simple as just using some
impl AsyncRead + AsyncWrite
. While HTTP/2 bundled the concept of stream creation internally, and thus could be managed wholly on top of a read-write transport, HTTP/3 is different. Stream creation is shifted to the QUIC protocol, and HTTP/3 needs to be able to use that directly. - This means the existing
Connection
types for both client and server will not be able to accept a QUIC transport so we can add HTTP/3 support.
- HTTP/3 will require a new transport abstraction. It's not as simple as just using some
It's not easy to match for specific errors.
The Error::source()
can leak an internal dependency. For example, a
hyper::Error
may wrap an h2::Error
. Users can downcast the source at
runtime, and hyper internally changing the version of its h2
dependency can
cause runtime breakage for users.
Formatting errors is in conflict with the current expected norm. The
fmt::Display
implementation for hyper::Error
currently prints its own
message, and then prints the message of any wrapped source error. The Errors
Working Group currently recommends that errors only print their own message
(link?). This conflict means that error "reporters", which crawl a source chain
and print each error, has a lot of duplicated information.
error fetching website: error trying to connect: tcp connect error: Connection refused (os error 61)
tcp connect error: Connection refused (os error 61)
Connection refused (os error 61)
While there is a good reason for why hyper's Error
types do this, at the very
least, it is unfortunate.
Note: this problem space, of who calls whom, will be explored more deeply in a future article.
At times, it's been wondered whether hyper should call user code, or if user
code should call hyper. For instance, should a Service
be called with a
request when the connection receives one, or should the user always poll for
the next request.
There's a similar question around sending a message body. Should hyper ask the
body for more data to write, or should the user call a write
method directly?
These both get at a root topic about write observability. How do you know when a response, or when body data, has been written successfully? This is desirable for metrics, or for triggering other side-effects.
The Service
trait also has some other frequently mentioned issues. Does
poll_ready
pull its complexity weight for servers? What about returning
errors, what does that mean? Ideally users would turn all errors into
appropriate http::Response
s. But in HTTP/2 and beyond, stream errors are
different from HTTP Server Error responses. Could the Service::Error
type do
more to encourage best practices?
The goal is to get hyper closer to the VISION, using that to determine the best way to solve the known issues above. The main thrust of the proposed changes are to make hyper more Flexible and stable.
In order to keep hyper Understandable, however, the proposed changes must
be accompanied by providing utilities that solve the common usage patterns,
documentation explaining how to use the more flexible pieces, and guides on how
to reach for the hyper-util
ity belt.
The majority of the changes are smaller and can be contained to the Public API section, since they usually only apply to a single module or type. But the biggest changes are explained in detail here.
The existing Connection
types, both for the client and server, abstract over
HTTP version by requiring a generic AsyncRead + AsyncWrite
transport type.
But as we figure out HTTP/3, that needs to change. So to prepare now, the
Connection
types will be split up.
For example, there will now be hyper::server::conn::http1::Connection
and
hyper::server::conn::http2::Connection
types.
These specific types will still have a very similar looking API that, as the VISION describes, provides Correct connection management as it pertains to HTTP.
There will be still be a type to wrap the different versions. It will no longer
be generic over the transport type, to prepare for being able to wrap HTTP/3
connections. Exactly how it will wrap, either by using internal trait objects,
or an enum Either
style, or using a trait Connection
that each type
implements, is something to be determined. It's likely that this "auto" type
will start in hyper-util
.
As mentioned in the Known Issues, the higher-level Client
and Server
have
stability and complexity problems. Therefore, for hyper 1.0, the main API will
focus on the "lower-level" connection types. The Client
and Server
helpers
will be moved to hyper-util
.
The Body
struct is removed. Its internal "variants" are separated into
distinct types, and can start
in either hyper-util
or http-body-util
.
The exported trait HttpBody
is renamed to Body
.
A single Body
implementation in hyper
is the one provided by receiving
client responses and server requests. It has the name Streaming
.
Unresolved: Other names can be considered during implementation. Another option is to not publicly name the implementation, but return
Response<impl Body>
s.
The Body
trait will be experimented on to see about making it possible to
return more frame types beyonds just data and trailers.
Unresolved: What exactly this looks like will only be known after experimentation.
The high-level hyper::Client
will be removed, along with the
hyper::client::connect
module. They will be explored more in hyper-util
.
As described in Design, the client::conn
module will gain http1
and
http2
sub-modules, providing per-version SendRequest
, Connection
, and
Builder
structs. An auto
version can be explored in hyper-util
.
The hyper::Error
struct remains in place.
All errors returned from Error::source()
are made opaque. They are wrapped an
internal Opaque
newtype that still allows printing, but prevents downcasting
to the internal dependency.
A new hyper::error::Code
struct is defined. It is an opaque struct, with
associated constants defining various code variants.
Alternative: define a non-exhaustive enum. It's not clear that this is definitely better, though. Keeping it an opaque struct means we can add secondary parts to the code in the future, or add bit flags, or similar extensions.
The purpose of Code
is to provide an abstraction over the kind of error that
is encountered. The Code
could be some behavior noticed inside hyper, such as
an incomplete HTTP message. Or it can be "translated" from the underlying
protocol, if it defines protocol level errors. For example, an
h2::Reason::CANCEL
.
The Executor
trait stays in here.
Define a new trait Timer
, which describes a way for users to provide a source
of sleeping/timeout futures. Similar to Executor
, a new generic is added to
connection builders to provide a Timer
.
The higher-level hyper::Server
struct, its related Builder
, and the
Accept
trait are all removed.
The AddrStream
struct will be completely removed, as it provides no value but
causes binary bloat.
Similar to client
, and as describe in the Design, the conn
modules will
be expanded to support http1
and http2
submodules. An auto
version can be
explored in hyper-util
.
A vendored and simplified Service
trait will be explored.
The error type for Service
s used for a server will explore having the return
type changed from any error to one that can become a hyper::error::Code
.
Unresolved: Both of the above points are not set in stone. We will explore and decide if they are the best outcome during development.
The MakeService
pieces will be removed.
Remove the stream
feature. The Stream
trait is not stable, and we cannot
depend on an unstable API.
Remove the tcp
and runtime
features. The automatic executor and timer parts
are handled by providing implementations of Executor
and Timer
. The
connect
and Accept
parts are also moving to hyper-util
.
http
http-body
bytes
Cannot be public while "unstable":
tracing
A channel implementation of Body
that has an API to know when the data has
been successfully written is provided in hyper_util::body::channel
.
A Pool
struct that implements Service
is provided. It fills a similar role
as the previous hyper::Client
.
Note: The
Pool
might be something that goes into thetower
crate instead. Or it might stay here as a slightly more specialized racing-connect pool. We'll find out as we go.
A connect
submodule that mostly mirrors the existing hyper::client::connect
module is moved here. Connectors can be used as a source to provide Service
s
used by the Pool
.
We can provide Tokio-backed implementations of Executor
and Timer
.
A GracefulShutdown
helper is provided, to allow for similar style of graceful
shutdown as the previous hyper::Server
did, but with better control.
There are some parts of the proposal which are not fully resolved. They are mentioned in Design and API sections above, but also collected here for easy finding. While they all have plans, they are more exploratory parts of the API, and thus they have a higher possibility of changing as we implement them.
The goal is to have these questions resolved and removed from the document by the time there is a Release Candidate.
Depending on tokio
just for AsyncRead
and AsyncWrite
is convenient, but
can be confusing for users integrating hyper with other runtimes. It also ties
our version directly to Tokio. We can consider having vendored traits, and
providing Tokio wrappers in hyper-util
.
We will experiment with this, and keep track of those experiments in a dedicated issue. It might be possible to use something like this:
pub trait Body {
type Data;
fn poll_frame(..) -> Result<Option<Frame<Self::Data>>>;
}
pub struct Frame<T>(Kind<T>);
enum Kind<T> {
Data(T),
Trailers(HeaderMap),
Unknown(Box<dyn FrameThingy>),
}
- There's still a few uncertain decisions around tower, such as if it should be
changed to
async fn call
, and ifpoll_ready
is the best way to handle backpressure. - It's not clear that the backpressure is something needed at the
Server
boundary, thus meaning we should removepoll_ready
from hyper. - It's not 100% clear if we should keep the service pattern, or use a pull-based API. This will be explored in a future blog post.
Naming is hard. We certainly should solve it, but discussion for particular names for structs and traits should be scoped to the specific issues. This document is to define the shape of the library API.
The hyper-util
crate will not reach 1.0 when hyper
does. Some types and
traits are being moved to hyper-util
. As with any pre-1.0 crate, you can
publicly depend on it, but it is explicitly less stable.
In most cases, it's recommended to not publicly expose your dependency on
hyper-util
. If you depend on a trait, such as used by the moved higher-level
Client
or Server
, it may be better for your users to define your own
abstraction, and then make an internal adapter.
We are making hyper more flexible. As noted in the VISION, most use cases of hyper require it to be flexible. That can mean that the exposed API is lower level, and that it feels more complicated. It should still be understandable.
But the hyper 1.0 effort is more than just the single hyper
crate. Many
useful helpers will be migrated to a hyper-util
crate, and likely improved in
the process. The timeline also points out that we will have a significant
documentation push. While the flexible pieces will be in hyper to compose how
they need, we will also write guides for the hyper.rs showing people how to
accomplish the most common tasks.