A Rust prototype exploring a single unified interface for proxying HTTP requests, SSE streams, bidirectional WebSockets, and (planned) WebTransport — all through one method.
An outbound API gateway sits between your service and upstream APIs. It resolves endpoints, authenticates, rate-limits, and forwards traffic. The challenge: HTTP, SSE, WebSocket, and WebTransport have very different semantics, yet from a gateway's perspective they all follow the same pattern — receive a request, forward it upstream, return a response.
This prototype proves that a single trait method can unify all of them:
#[async_trait]
pub trait GatewayProxy: Send + Sync {
async fn proxy_request(
&self,
req: http::Request<Body>,
) -> Result<http::Response<Body>, StreamingError>;
}The trick is the Body enum:
pub enum Body {
Empty, // GET, HEAD, DELETE — no payload
Bytes(Bytes), // Regular HTTP — buffered JSON, form data, etc.
Stream(BodyStream), // SSE, WebSocket messages, chunked uploads
}Each protocol maps naturally onto Request<Body> → Response<Body>:
| Protocol | Request Body | Response Body |
|---|---|---|
| HTTP | Bytes or Empty |
Bytes |
| SSE | Bytes or Empty |
Stream (events) |
| WebSocket | Stream (→ upstream) |
Stream (← upstream) |
| WebTransport | Stream |
Stream |
No framework types leak into the gateway trait. The handler layer (e.g. axum) converts framework-specific types at the edges — the proxy itself works exclusively with http crate
types and Body.
The proxy inspects the request/response to dispatch automatically:
- WebSocket — detected via
Connection: Upgrade+Upgrade: websocketheaders on the request - SSE — detected via
Content-Type: text/event-streamon the upstream response - HTTP — everything else; response body is buffered into
Bytes
src/
├── lib.rs # Public API exports
├── error.rs # StreamingError enum
├── gateway/
│ ├── proxy.rs # GatewayProxy trait
│ ├── mock_proxy.rs # Reference implementation (reqwest + tungstenite)
│ ├── body.rs # Body enum, BodyStream type alias
│ ├── detect.rs # is_websocket_upgrade()
│ └── sse_stream.rs # ServerEventsStream<T>, FromSseEvent trait
├── sse/
│ ├── event.rs # SseEvent struct (W3C-compliant fields)
│ ├── detect.rs # is_sse_response()
│ ├── parse.rs # parse_sse_stream() — chunk-level SSE parser
│ └── response.rs # sse_response() — rebuild SSE from event stream
└── ws/
├── upstream.rs # connect_upstream(), UpstreamWs type
└── bridge.rs # bridge_websocket() — bidirectional forwarding
The core abstraction. Implement this to build a gateway that handles all supported protocols through proxy_request. The included MockGatewayProxy is a reference implementation
that forwards to a real upstream using reqwest (HTTP/SSE) and tokio-tungstenite (WebSocket).
Body abstracts over empty, buffered, and streaming payloads. Conversions go both ways:
body.into_bytes()— buffers a stream intoBytes(async)body.into_stream()— wraps any variant as aBodyStream
parse_sse_stream(body)— parses a raw byte stream intoSseEvents following the W3C EventSource spec (multi-line data, comments, retry, event types)sse_response(events)— serializes an event stream back into an HTTP response with correct headers (text/event-stream,no-cache,X-Accel-Buffering: no)ServerEventsStream<T>— ergonomic wrapper that detects SSE from aResponse<Body>, parses events, and optionally deserializes them into a custom type viaFromSseEventsse_json_event!(MyType)— one-line macro to implementFromSseEventfor JSON-deserializable types
connect_upstream(url, headers)— establishes a WebSocket connection to an upstream, forwarding custom headers (auth, etc.)bridge_websocket(client_ws, url, headers)— bidirectional frame forwarding between an axumWebSocketand an upstream, handling Text, Binary, Ping/Pong, and Close frames viatokio::select!
let req = http::Request::builder()
.method("POST")
.uri("/echo")
.header("content-type", "application/json")
.body(Body::Bytes(Bytes::from(r#"{"hello":"world"}"#)))
.unwrap();
let resp = gateway.proxy_request(req).await?;
let bytes = resp.into_body().into_bytes().await?;#[derive(Deserialize)]
struct ChatChunk {
delta: String
}
sse_json_event!(ChatChunk);
let resp = gateway.proxy_request(req).await?;
let mut stream = ServerEventsStream::<ChatChunk>::from_response(resp)
.expect("expected SSE response");
while let Some(Ok(chunk)) = stream.next().await {
print!("{}", chunk.delta);
}// In the handler layer, convert axum WebSocket frames to/from Body::Stream,
// then call proxy_request — the gateway sees the same trait interface.
let req = http::Request::builder()
.uri("/ws/endpoint")
.header("connection", "Upgrade")
.header("upgrade", "websocket")
.body(Body::Stream(client_to_upstream_stream))
.unwrap();
let resp = gateway.proxy_request(req).await?;
// resp.into_body() is Body::Stream with upstream messagesThe test suite covers all protocols end-to-end with a MockServer that provides HTTP echo, SSE, and WebSocket endpoints:
cargo testTests include HTTP round-trips, SSE parsing and serialization, SSE-to-JSON deserialization, WebSocket echo and streaming, bidirectional bridging, body conversions, and handler dispatch patterns.
This is a prototype. It validates the unified proxy_request design across HTTP, SSE, and WebSocket. WebTransport support is planned but not yet implemented.
| Crate | Purpose |
|---|---|
http |
Framework-agnostic HTTP types |
bytes, futures-core, futures-util |
Stream and byte abstractions |
reqwest (stream) |
HTTP client for upstream requests |
tokio-tungstenite (rustls) |
WebSocket client for upstream connections |
axum (ws) |
Web framework for handler layer and tests |
tokio |
Async runtime |
serde, serde_json |
JSON serialization |
async-trait |
Async trait support |
thiserror |
Error derive macro |
tracing |
Structured logging |