Building a High-Performance HTTP Proxy Server (Route Based) in Rust with Hyper & Tokio

Md.Aminul Islam Sarker
4 min readMar 3, 2023

--

Building a High-Performance HTTP Proxy Server in Rust with Hyper & Tokio

If you’re looking to build a high-performance, secure HTTP proxy server, Rust with the hyper and tokio crates provides a powerful combination to get you started. Rust's powerful memory safety and concurrency features make it ideal for building robust network applications, while hyper provides a flexible and easy-to-use interface for building HTTP servers and clients. tokio is a runtime for writing reliable, asynchronous, and slim applications with high I/O throughput.

In this article, we’ll walk through how to build a simple HTTP proxy server in Rust with hyper and tokio. We'll cover the basics of building an HTTP server with hyper, making requests to a target URL with tokio, and forwarding incoming requests to the target URL. By the end of this tutorial, you'll have a solid foundation for building high-performance HTTP proxy servers with Rust, hyper, and tokio.

$ cargo new http-proxy-server
$ cd http-proxy-server
$ echo 'hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http = "0.2"' >> Cargo.toml
$ cargo update

Next, we’ll define a simple proxy server that listens on a specified port and forwards incoming requests to a target URL. Our server will use the make_service_fn method from the hyper::service module to create a new service that handles incoming requests and passes them to the proxy function for processing:

#![deny(warnings)]

use std::convert::Infallible;
use std::net::SocketAddr;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Client, Request, Response, Server};

type HttpClient = Client<hyper::client::HttpConnector>;

#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 8100));

let client = Client::builder()
.http1_title_case_headers(true)
.http1_preserve_header_case(true)
.build_http();

let make_service = make_service_fn(move |_| {
let client = client.clone();
async move { Ok::<_, Infallible>(service_fn(move |req| proxy(client.clone(), req))) }
});

let server = Server::bind(&addr)
.http1_preserve_header_case(true)
.http1_title_case_headers(true)
.serve(make_service);

println!("Listening on http://{}", addr);

if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}

Our main function sets up a new HTTP server that listens on the specified socket address. We use the Client struct from the hyper crate to create an HTTP client that we'll use to forward requests to the target URL.

The make_service_fn function creates a new service that handles incoming requests and passes them to the proxy function for processing. We use Rust's async/await syntax to define an asynchronous function that returns a Result containing either a valid service or an Infallible error.

The Server::bind method creates a new HTTP server that listens on the specified socket address, and the Server::serve method starts the server and serves incoming requests using the specified service.

Next, let’s define our proxy function, which will handle incoming requests and forward them to the target URL:

async fn proxy(_client: HttpClient, req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
let headers = req.headers().clone();
println!("headers: {:?}", headers);

let path = req.uri().path().to_string();
if path.starts_with("/hello") {
let target_url = "http://127.0.0.1:8000".to_owned();

In the proxy function, we first clone the incoming request headers and print them to the console. Next, we extract the request path and check if it starts with "/hello". If it does, we create a new HTTP request to the target URL using the get_response function and return the response to the client.

If the request path does not start with “/hello”, we return a “no route found” error message to the client. Here’s the updated proxy function:

async fn proxy(_client: HttpClient, req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
let headers = req.headers().clone();
println!("headers: {:?}", headers);

let path = req.uri().path().to_string();
if path.starts_with("/hello") {
let target_url = "http://127.0.0.1:8000".to_owned();
let resp = get_response(_client, req, &target_url, &path).await?;
return Ok(resp);
}

let resp = Response::new(Body::from("sorry! no route found"));
Ok(resp)
}

The get_response function creates a new HTTP request to the target URL and sends it using the client.request method. We then wait for the response and return it to the caller. Here's the updated get_response function:

async fn get_response(client: HttpClient, req: Request<Body>, target_url: &str, path: &str) -> Result<Response<Body>, hyper::Error> {
let target_url = format!("{}{}", target_url, path);
let headers = req.headers().clone();
let mut request_builder = Request::builder()
.method(req.method())
.uri(target_url)
.body(req.into_body())
.unwrap();

*request_builder.headers_mut() = headers;
let response = client.request(request_builder).await?;
let body = hyper::body::to_bytes(response.into_body()).await?;
let body = String::from_utf8(body.to_vec()).unwrap();

let mut resp = Response::new(Body::from(body));
*resp.status_mut() = http::StatusCode::OK;
Ok(resp)
}

The get_response function first constructs the full target URL by concatenating the target URL and the incoming request path. We then clone the incoming request headers and use them to construct a new HTTP request with the same method, URI, and body as the incoming request.

We use the client.request method to send the new HTTP request to the target URL, wait for the response, and convert the response body to a string. Finally, we create a new response with the same body as the target response and return it to the caller.

That’s it! With just a few lines of code, we’ve created a basic HTTP proxy server in Rust that can forward incoming requests to a target URL. You can extend this code to add additional features like caching, authentication, and load balancing.

To run this code, simply save it to a file called main.rs in a new Rust project and run the following command:

$ cargo run

This will start the HTTP proxy server and listen for incoming requests on the specified port. You can test the server by opening a web browser and navigating to http://localhost:8100/hello. The server should forward the request to the target URL and return the response to the browser.

Conclusion

In this article, we walked through how to build a high-performance HTTP proxy server in Rust with the hyper and tokio crates. We covered the basics of building an HTTP server with hyper, making requests to a target URL with tokio, and forwarding incoming requests to the target URL.

We hope this tutorial has been helpful in getting you started with building HTTP proxy servers in Rust. If you have any questions or feedback, please feel free to leave a comment below.

Happy coding!

--

--

Md.Aminul Islam Sarker
Md.Aminul Islam Sarker

Written by Md.Aminul Islam Sarker

Seasoned IT pro with a passion for web dev, software engineering & project management. Skilled in Rust, PHP, JS, Java & AWS. Let's explore tech together!

No responses yet