Building a High-Performance HTTP Proxy Server (Route Based) 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!