DEV Community

Cover image for Basic http proxy in rust with ntex
Leone
Leone

Posted on

Basic http proxy in rust with ntex

What is ntex ?

Ntex is a powerful, pragmatic, and extremely fast framework for composable networking services for Rust.
It's one of the fastest web frameworks available in Rust, and provides powerful abstractions for web server development.

Why ntex ?

This are my top reasons for using ntex:

  • Performance: Ntex is one of the fastest web frameworks available in Rust.
  • Ergonomic: Ntex provides powerful abstractions for web server development.
  • Composable: Ntex is designed to be composable, allowing you to build complex web servers from simple components.
  • Ecosystem: Ntex has a rich ecosystem of middleware, extensions and libraries.
  • Built-in http client: Ntex provides a built-in http client for making requests to other servers.
  • Runtime: Ntex allow you to choose between different runtimes, including tokio and async-std.

Setting up the project

Let's start by creating a new project with cargo:

cargo new ntex-http-proxy
cd ntex-http-proxy
Enter fullscreen mode Exit fullscreen mode

Add ntex as dependency:

cargo add ntex --features tokio
Enter fullscreen mode Exit fullscreen mode

Starting with a basic http handler

Let's start by creating a basic http handler that will return Hello, World! in plain text format:

use ntex::{http, web};

async fn forward() -> Result<web::HttpResponse, web::Error> {
  Ok(
    web::HttpResponse::Ok()
      .content_type("text/plain")
      .body("Hello, world!"),
  )
}

#[ntex::main]
async fn main() -> std::io::Result<()> {
  web::server(move || {
    web::App::new()
      .state(http::Client::new())
      .wrap(web::middleware::Logger::default())
      .default_service(web::route().to(forward))
  })
  .bind(("0.0.0.0", 9090))?
  .stop_runtime()
  .run()
  .await
}
Enter fullscreen mode Exit fullscreen mode

Let's break down the code:

  • forward is an async function that returns a web::HttpResponse or a web::Error.
  • main is the entry point of our application. It's an async function that returns a std::io::Result<()>.
  • We create a new ntex web server with web::server and pass a closure that returns a web::App.
  • We create a new http::Client and add it to the app state.
  • We add a logger middleware to the app.
  • We define a default service that will forward all requests to the forward handler.
  • We bind the server to 0.0.0.0:9090 and run it.

Let's test the server by running it:

cargo run
curl http://localhost:9090
Enter fullscreen mode Exit fullscreen mode

You should see Hello, world! in the response.

Adding a proxy handler

We start by adding the url and futures_util crates to our dependencies to be able to parse urls and convert responses to streams:

cargo add url futures-util
Enter fullscreen mode Exit fullscreen mode

Then we change the the code to forward requests to another server:

use futures_util::TryStreamExt;
use ntex::{http, web};

async fn forward(
  req: web::HttpRequest,
  body: ntex::util::Bytes,
  client: web::types::State<http::Client>,
  forward_url: web::types::State<url::Url>,
) -> Result<web::HttpResponse, web::Error> {
  let mut new_url = forward_url.get_ref().clone();
  new_url.set_path(req.uri().path());
  new_url.set_query(req.uri().query());
  let forwarded_req = client.request_from(new_url.as_str(), req.head());
  let res = forwarded_req
    .send_body(body)
    .await
    .map_err(web::Error::from)?;
  let mut client_resp = web::HttpResponse::build(res.status());
  let stream = res.into_stream();
  Ok(client_resp.streaming(stream))
}

#[ntex::main]
async fn main() -> std::io::Result<()> {
  let forward_url = "https://www.rust-lang.org".to_owned();
  let forward_url = url::Url::parse(&forward_url)
    .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
  web::server(move || {
    web::App::new()
      .state(http::Client::new())
      .state(forward_url.clone())
      .wrap(web::middleware::Logger::default())
      .default_service(web::route().to(forward))
  })
  .bind(("0.0.0.0", 9090))?
  .run()
  .await
}
Enter fullscreen mode Exit fullscreen mode

Let's break down the code:

  • We add the url and futures_util crates to our dependencies.
  • We change the forward function to take the request, body, client and forward_url as arguments.
  • We create a new url by cloning the forward_url and setting the path and query from the request.
  • We create a new request using the client and the new url.
  • We send the body of the request and await the response.
  • We build a new response with the status code of the response.
  • We convert the response into a stream and return it.

Let's test the server by running it:

cargo run
curl http://localhost:9090
Enter fullscreen mode Exit fullscreen mode

You should see the rust-lang.org homepage in the response.

Conclusion

In this tutorial, we created a basic http proxy server using ntex. We started by creating a simple http handler that returns Hello, World! in plain text format. Then we added a proxy handler that forwards requests to another server. We used the url and futures_util crates to parse urls and convert responses to streams. We tested the server by running it and making a request to it. We saw that the server successfully forwarded the request to the target server and returned the response.
We have little to no code to write a basic http proxy server (less than 50 lines of code) and we can easily extend it with more features like caching, rate limiting, authentication, etc.

I hope you enjoyed this tutorial and found it useful. If you have any questions or feedback, feel free to leave a comment below.

Top comments (0)