hyper (Rust) upgrade to v1: Higher-level Server / Client were removed

RMAG news

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 🙂

Upgrade from v0.14 to v1
Roadmap to v1
Getting Started with a Server
hyper/examples/web_api.rs