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).