hyper (Rust) upgrade to v1: Higher-level Server / Client were removed
Tags: rust, hyper, api, microservices
Published: 2024-05-28 02:12:05

## 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](https://hyper.rs/contrib/roadmap/#focus-on-the-connection-level).

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`](https://docs.rs/hyper-util/latest/hyper_util/index.html) helps.

### My project challenge

[apimock-rs](https://github.com/nabbisen/apimock-rs) is API mock Server generating HTTP / JSON responses to help to develop microservices and APIs, written in [Rust](https://www.rust-lang.org/). It's one of my projects.

Its core dependencies is [hyper](https://hyper.rs/), "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](https://github.com/hyperium/hyper/releases/tag/v1.0.0) 🎉

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*:

```diff
  [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*:

```diff
  [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:

```diff
- 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.

```diff
- 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](https://hyper.rs/guides/1/upgrading/)
- [Roadmap to v1](https://hyper.rs/contrib/roadmap/)
- [Getting Started with a Server](https://hyper.rs/guides/1/server/hello-world/)
- [hyper/examples/web_api.rs](https://github.com/hyperium/hyper/blob/master/examples/web_api.rs)