Tower interop

Use tonic gRPC services and tower-http layers in an rws app, and serve an rws app from a Tower stack — opt-in adapters, with rws's own request path unchanged.

Requires the tower feature:

[dependencies]
rust-web-server = { version = "18", features = ["tower"] }

Tower's Service and Layer traits are what tonic, tower-http, hyper and axum are built on. rws doesn't run on them — its own Application, Middleware, Request and Response keep the fast paths (handlers inline on the event loop, sendfile, per-core runtimes) — but the tower feature adds adapters both ways. Without the feature nothing changes, and with it only the routes and layers you adapt go through Tower.

Adapter Turns Into
tower::mount(prefix, service, app) / TowerApp::new(service) a Tower service — a tonic server, tower-http's ServeDir, an axum Router an rws Application
TowerLayer::new(layer) a Tower layer — tower-http's SetResponseHeaderLayer, ValidateRequestHeaderLayer, NormalizePathLayer, … rws middleware (.wrap(...))
TowerService::new(app) an rws app a Tower Service, for hyper or axum

Requests and responses cross as http::Request<tower::Body> / http::Response<_>.

A tonic service

use rust_web_server::prelude::*;
use rust_web_server::tower::mount;

let (_reporter, health) = tonic_health::server::health_reporter();
let app = mount("/grpc.health.v1.Health/", health, App::new());

Requests whose path starts with the prefix go to the service, the rest to the app. A tonic service needs HTTP/2: rws serves it over TLS (ALPN h2), and over cleartext to clients that speak HTTP/2 with prior knowledge (grpcurl -plaintext, tonic's own client). Bodies stream both ways — the request as it arrives (client streaming and bidirectional calls), the response as the service produces it, with grpc-status in the trailers; a trailers-only answer (an error) ends at the headers, as gRPC clients expect. Several services: mount them in front of each other.

tower-http layers

use rust_web_server::tower::TowerLayer;
use tower_http::set_header::SetResponseHeaderLayer;

let app = App::with_state(state)
    .get("/hello", hello)
    .wrap(TowerLayer::new(SetResponseHeaderLayer::overriding(
        http::header::HeaderName::from_static("x-served-by"),
        http::HeaderValue::from_static("rws"),
    )));

Per request the layer wraps the rest of the stack (tower::Next), sees the request as an http::Request, and the response comes back through it. Streamed responses — files, SSE, proxied streams — stay streams; request bodies reach the layer read into memory.

The rest of the stack is synchronous rws code, so a layer that waits on its inner service can't interrupt it: tower-http's TimeoutLayer won't cut a slow handler short (use rws's TimeoutLayer, which has the same limit for synchronous handlers, or an async handler). Compression and CORS are better left to rws's own — they're built in and tuned for the static-file path.

An rws app in a Tower stack

use rust_web_server::tower::TowerService;

let service = TowerService::new(app);
// hyper: serve_connection(io, hyper_util::service::TowerToHyperService::new(service))
// axum:  Router::new().route(...).fallback_service(service)

The app runs as rws runs it — execute_async when it offers one, inline when it says it can, otherwise on the blocking pool — so call it inside a tokio runtime. The request body is read into memory first; an app error becomes a 500. Tower carries no peer address, so handlers see the ConnectionInfo given to .connection(...) (by default an unknown peer; axum's ConnectInfo isn't read).

How it runs

Tower futures run on one process-wide runtime, rws-tower (two threads), because tokio I/O and timers belong to the runtime that created them and an rws handler may run under the shared runtime, a per-core one, or none (TestClient). A TowerLayer's future borrows the request's stack, so it's driven on the request's own thread inside that runtime's context. Its threads carry no trace context: automatic client spans don't nest under requests a Tower service answers.

What each conversion does: headers http refuses (rare; obs-text is kept) are left out rather than failing the request; Content-Length, Transfer-Encoding and hop-by-hop headers are dropped and regenerated by rws; a response body is collected when it's small (≤ 1 MiB) with a known length and not gRPC, and streamed otherwise.

examples/tower.rs mounts tonic-health and a tower-http layer; ci/e2e/tower.sh calls it with grpcurl over h2c (unary, a trailers-only error, server streaming).

Edit this page on GitHub