WebSocket Proxying at the Edge

FastEdge workers aren't limited to just responding to incoming requests — they can also make outbound network requests to external services. This opens up a world of possibilities: aggregating data from multiple APIs, proxying requests to upstream services, fetching remote content for caching, and more.

This post covers WebSocket Proxying at the Edge — a fundamental pattern for any edge application that needs to communicate with external services.

How It Works

Proxy and manage WebSocket connections at the edge — terminate WebSocket upgrades, authenticate before upgrading, and route to the right backend.

The FastEdge Rust SDK provides two approaches for making outbound requests:

  1. Synchronous (fastedge::http_client) — simple, blocking API that works within the standard Request/Response handler model. Best for straightforward fetch-and-return scenarios.
  2. Asynchronous (proxy-wasm dispatch) — uses the proxy-wasm ABI to pause request processing, make an HTTP call, and resume. Best for intercepting and augmenting client requests with external data.

Approach 1: Synchronous HTTP Client

The simplest approach — build a Request, send it, and get a Response back:

use fastedge::{
    body::Body,
    http::{Request, Response, StatusCode, Error},
    http_client,
};

#[fastedge::http]
fn main(req: Request<Body>) -> Result<Response<Body>, Error> {
    // Build an outbound request to an external API
    let upstream = Request::builder()
        .method("GET")
        .uri("https://httpbin.org/ip")
        .header("User-Agent", "fastedge-worker")
        .header("Accept", "application/json")
        .body(Body::empty())
        .map_err(|_| Error::InvalidBody)?;

    // Send it from the edge
    let resp = http_client::send_request(upstream)?;

    // Forward the upstream response headers plus our own
    let status = resp.status();
    let body = resp.into_body();

    Response::builder()
        .status(status)
        .header("content-type", "application/json")
        .header("x-edge-cached", "false")
        .body(body)
}

Approach 2: Async Proxy-Wasm Dispatch

For more advanced scenarios where you need to intercept a client request and augment it with external data:

use fastedge::proxywasm::*;
use std::time::Duration;

struct MyHttp { state: u32 }

impl Context for MyHttp {}

impl HttpContext for MyHttp {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        // Already got the data, continue the request
        if self.state == 1 {
            return Action::Continue;
        }

        // Pause the client request and fetch external data
        match self.dispatch_http_call(
            "api.example.com",           // authority
            vec![                       // headers
                (":scheme", "https"),
                (":authority", "api.example.com"),
                (":path", "/auth/check"),
                ("Authorization", "Bearer token123"),
            ],
            None,                        // no body
            vec![],                      // no trailers
            Duration::from_secs(5),      // timeout
        ) {
            Ok(token) => Action::Pause,
            Err(status) => {
                self.send_http_response(502, vec![], b"Upstream unavailable");
                Action::Pause
            }
        }
    }

    fn on_http_call_response(
        &mut self,
        token_id: u32,
        num_headers: usize,
        body_size: usize,
        _: usize,
    ) {
        if num_headers > 0 {
            let body = self.get_http_call_response_body(0, body_size);
            // Store the response data and resume the client request
            self.state = 1;
            self.resume_http_request();
        } else {
            // HTTP call failed, reset
            self.reset_http_request();
        }
    }
}

Common Use Cases

Error Handling and Timeouts

Outbound HTTP calls are subject to network conditions. Always handle failures gracefully:

match http_client::send_request(upstream) {
    Ok(resp) => handle_success(resp),
    Err(e) => {
        logging::error!("Upstream call failed: {}", e);
        // Return a cached or fallback response instead of failing
        Response::builder()
            .status(StatusCode::OK)
            .header("content-type", "text/plain")
            .header("x-fallback", "true")
            .body(Body::from("Service temporarily unavailable"))
    }
}
Caveat: Outbound requests from edge workers are subject to provider-configured timeouts (typically 5-30 seconds depending on your plan). Plan for failures with retries, circuit breakers, and fallback responses. Never assume an outbound call will succeed.