Documentation
Language Guide
Rust
Tutorials
WASIX with Reqwest

WASIX with Reqwest

This is a sample project that shows how to use a reqwest client to build an outbound proxy and compile it to WASIX.

Prerequisites

⚠️

Please check that you have the latest version of wasmer runtime as this tutorial depends on version 4.1.1 or higher.

The project requires the following tools to be installed on your system:

Start a new project

$ cargo new --bin wasix-reqwest-proxy
     Created binary (application) `wasix-reqwest-proxy` package

Your wasix-reqwest-proxy directory structure should look like this:

Add dependencies

$ cd wasix-reqwest-proxy
$ cargo add axum
$ cargo add tokio --features rt-multi-thread,macros
$ cargo add reqwest --no-default-features --features rustls-tls

Now your Cargo.toml should look like this:

Cargo.toml
[package]
name = "wasix-reqwest-proxy"
version = "0.1.0"
edition = "2021"
 
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
ℹ️

Plain versions from crates.io — no git URLs, no pins, no [patch.crates-io] section. reqwest and its whole TLS stack (hyper, rustls, ring, …) resolve as WASIX-ready builds through the WASIX registry, transitively.

Writing the Application

Our outbound proxy application will have two parts:

  1. Listen for incoming requests using an axum server
  2. Forward the request to the destination using the reqwest client and return the response to the client

Part 1. - Listening for incoming requests

Let's set up a basic axum server that listens on port 3000 and sends every request to a handle function.

src/main.rs
#[tokio::main]
async fn main() {
    // Send every request, whatever its path, to our handler...
    let app = Router::new().fallback(any(handle));
    //                                    ^^^^^^
    // 🔦 Focus here - This handle function is what connects to the part 2 of our application.
 
    // ...and serve on http://127.0.0.1:3000
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("Listening on http://127.0.0.1:3000");
    axum::serve(listener, app).await.unwrap();
}

Part 2. - Forwarding the request to the destination

Now let's write the handle function that will be called for each incoming request. This function will use the reqwest client to forward the request to the destination and return the response to the client.

src/main.rs
async fn handle(req: Request) -> Response {
    // Create the destination URL
    let url = format!(
        "https://www.rust-lang.org{}",
        req.uri()
            .path_and_query()
            .map(|p| p.as_str())
            .unwrap_or("/")
    ); // ← 1.
 
    let (status, body) = match fetch(&url).await {
        Ok(body) => (StatusCode::OK, body),
        Err(err) => (
            err.status().unwrap_or(StatusCode::BAD_GATEWAY),
            err.to_string(),
        ),
    }; // ← 2.
 
    Response::builder()
        .status(status)
        .body(Body::from(body))
        .unwrap() // ← 3.
}
 
async fn fetch(url: &str) -> Result<String, reqwest::Error> {
    reqwest::get(url).await?.text().await
}

Let's go through the code above:

  1. Create a url variable that contains the destination URL. We use req.uri() to get the request path and append it to the destination host.
  2. Use the reqwest client (via our little fetch helper) to make the request and read the response body. If the request fails, we use the error's status (or BAD_GATEWAY) and return the error message as the body.
  3. Build the response with the status and body and return it.

Your src/main.rs should now look like this:

src/main.rs
use axum::{
    body::Body, extract::Request, http::StatusCode, response::Response, routing::any, Router,
};
 
async fn handle(req: Request) -> Response {
    let url = format!(
        "https://www.rust-lang.org{}",
        req.uri()
            .path_and_query()
            .map(|p| p.as_str())
            .unwrap_or("/")
    );
 
    let (status, body) = match fetch(&url).await {
        Ok(body) => (StatusCode::OK, body),
        Err(err) => (
            err.status().unwrap_or(StatusCode::BAD_GATEWAY),
            err.to_string(),
        ),
    };
 
    Response::builder()
        .status(status)
        .body(Body::from(body))
        .unwrap()
}
 
async fn fetch(url: &str) -> Result<String, reqwest::Error> {
    reqwest::get(url).await?.text().await
}
 
#[tokio::main]
async fn main() {
    let app = Router::new().fallback(any(handle));
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("Listening on http://127.0.0.1:3000");
    axum::serve(listener, app).await.unwrap();
}

Running the Application

Let's compile the application to WASIX and run it:

Compiling to WASIX
$ cargo wasix build
    Updating `.cargo/config.toml` to resolve crates through the WASIX registry
   Compiling tokio v1.47.0+wasix.1
   Compiling reqwest v0.12.22+wasix.1
   Compiling ring v0.17.14+wasix.1
   ...
   Compiling wasix-reqwest-proxy v0.1.0 (/wasix-reqwest-proxy)
    Finished `dev` profile [unoptimized + debuginfo] target(s)
info: Post-processing WebAssembly files
  Optimizing with wasm-opt

Note the +wasix.N versions: on the first build, cargo wasix writes the WASIX registry config into .cargo/config.toml, and the WASIX forks — including the whole TLS stack that reqwest needs — resolve transparently.

ℹ️

Commit the generated .cargo/config.toml so that other checkouts and CI build through the registry too.

Running the Application on WASIX
$ cargo wasix run -W,--net

cargo wasix run builds and runs the binary with the Wasmer runtime. The -W, prefix passes comma-separated flags through to the runtime — here --net, which enables networking support (see passing runtime flags for details).

Now in a separate terminal, you can use curl to make a request to the server:

$ curl localhost:3000
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>
        Rust Programming Language
        ...

Congratulations! You have successfully built an outbound proxy server — with real outbound TLS — using axum, reqwest and wasix.

ℹ️

You can also deploy your application to the edge. Checkout this tutorial (opens in a new tab) for deploying your wasix-reqwest-proxy server to wasmer edge.

Exercises

Exercise 1

Try to take the destination URL as a query parameter.

$ curl localhost:3000?url=https://www.rust-lang.org

Exercise 2

Try to take the destination URL as a parameter for the .wasm file.

$ cargo wasix run -W,--net -- --url=https://www.rust-lang.org
💡

You can use the -- to pass arguments to the .wasm file and use the rust's default std::env::args to parse the arguments.

Conclusion

In this tutorial, we learned:

  • How to build a simple outbound proxy server using axum and reqwest.
  • How WASIX crate forks — including the TLS stack — resolve automatically through the WASIX registry.
  • How to run wasix based .wasm files with Wasmer.
  • How to pass runtime flags like --net through cargo wasix run -W,.
wasix-rust-examples/wasix-reqwest-proxy