hyper (Rust) upgrade to v1: Higher-level Server / Client were removed
nabbisen
Posted on May 28, 2024
Summary
This series is about how I upgraded hyper (Rust) 0.14 to v1 (1.3).
The next theme is higher-level Server
/ Client
. Those in v0 were removed. It was because they had stability and complexity problems.
The Server
wasn't followed by any drop-in replacement, and the Client
was in a way by client::legacy::Client
(that I didn't use).
In addition, hyper-util
helps.
My project challenge
apimock-rs is API mock Server generating HTTP / JSON responses to help to develop microservices and APIs, written in Rust. It's one of my projects.
Its core dependencies is hyper, "a protective and efficient HTTP library for all" which is rather low-level.
Upgraded hyper
I started with hyper 0.14, and 1.0.0 was released last November 🎉
I have recently upgraded it which was a kind of somehow tough work. The change log was as below:
https://github.com/nabbisen/apimock-rs/pull/62/files
Cargo.toml
change log
As to HTTP server:
[dependencies]
(...)
- hyper = { version = "0.14", features = ["server", "http1", "http2", "tcp"] }
+ hyper = { version = "1", features = ["server", "http1", "http2"] }
+ hyper-util = { version = "^0.1", features = ["server", "http1", "http2", "tokio"] }
+ http-body-util = "^0.1"
As to HTTP client:
[dev-dependencies]
- hyper = { version = "0.14", features = ["client"] }
+ hyper = { version = "1", features = ["client"] }
Server
change log
hyper::Server
had gone. I used conn
module of auto
HTTP version in hyper-util
instead of the specific version in hyper
since I wanted to support both http1 and http2.
The diff was like:
- use hyper::service::{make_service_fn, service_fn};
- use hyper::Server;
+ use hyper::{body, body::Bytes, service::service_fn, Request, Response};
+ use hyper_util::{
+ rt::{TokioExecutor, TokioIo},
+ server::conn::auto::Builder,
+ };
+ use tokio::net::TcpListener;
(...)
let addr = (...)
- let make_svc = make_service_fn(|_| {
- async move {
- let service = service_fn(move |req| handle(req));
- Ok::<_, Infallible>(service)
- }
- });
-
- let server = Server::bind(&addr).serve(make_svc);
+ let listener = TcpListener::bind(addr)
+ .await
+ .expect("tcp listener failed to bind address");
+ loop {
+ let (stream, _) = listener
+ .accept()
+ .await
+ .expect("tcp listener failed to accept");
+ let io = TokioIo::new(stream);
+
+ tokio::task::spawn(async move {
+ if let Err(err) = Builder::new(TokioExecutor::new())
+ .serve_connection(
+ io,
+ service_fn(move |req: Request<body::Incoming>| service(req )),
+ )
+ .await
+ {
+ eprintln!("error serving connection: {:?}", err);
+ }
+ });
+ }
+
+ async fn service(
+ req: Request<body::Incoming>,
+ ) -> Result<Response<BoxBody>, hyper::http::Error> {
+ handle(req).await
+ }
As it is relatively lower-level module, tokio
requires to be dealt with together.
Client
change log
In contrast, on client, I used module in hyper
which supported the specific HTTP version. It was testing module whose HTTP version didn't affect the result.
- use hyper::{body::to_bytes, Body, Client, Request, Response, StatusCode, Uri};
- (...)
- let request = Request::builder()
- .uri(uri)
- .method("POST")
- .header("Content-Type", "text/plain")
- .body(Body::from(body.to_owned()))
- .unwrap();
- let client = Client::new();
- let response = client.request(request).await.unwrap();
+ let stream = TcpStream::connect(addr).await.unwrap();
+ let io = TokioIo::new(stream);
+ let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
+ tokio::task::spawn(async move {
+ if let Err(err) = conn.await {
+ println!("Connection failed: {:?}", err);
+ }
+ });
+ (...)
+ let req = Request::builder()
+ .uri(path)
+ .header(hyper::header::HOST, authority.as_str())
+ .body(body)
+ .unwrap();
+ let res = sender.send_request(req).await.unwrap()
hyper::client::conn::http1
is directly used as above.
Reference
Their official documentation and examples are really helpful :)
Posted on May 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.