Para colocar uma API Axum em produção com CORS, timeout, tracing, compressão e limite de corpo sem escrever middleware à mão, use tower-http: um único crate com as camadas HTTP mais usadas do ecossistema Tower. Adicione tower-http ao Cargo.toml com as features de que precisa, empilhe as camadas em um ServiceBuilder na ordem certa e o Router ganha comportamento operacional completo em poucas linhas. Este guia mostra o setup, cada camada essencial, a ordem correta de empilhamento, arquivos estáticos com ServeDir e os erros comuns.
tower-http já aparece espalhado pelo site — em serviços resilientes com Tower e Axum, em deploy com Docker Compose e em autenticação JWT — mas faltava a página que mostra o crate por inteiro. Esta é ela.
Resposta rápida: qual camada para cada problema
| Problema em produção | Camada do tower-http | Observação |
|---|---|---|
| Navegador bloqueando a API (preflight falha) | CorsLayer | origens, métodos e headers explícitos |
| Handler pendurado travando conexões | TimeoutLayer | cliente recebe 408 quando estoura |
| Não saber latência nem status por rota | TraceLayer + tracing | spans automáticos por requisição |
| Payload gigante derrubando o serviço | RequestBodyLimitLayer | rejeite cedo, antes de ler o corpo |
| Respostas grandes sem compressão | CompressionLayer | cuidado com streaming/SSE |
| Correlacionar requisições entre serviços | SetRequestIdLayer + PropagateRequestIdLayer | X-Request-Id ponta a ponta |
| Vazar headers sensíveis no log | SetSensitiveRequestHeadersLayer | esconde Authorization, Cookie |
| Servir SPA e assets no mesmo binário | ServeDir | com fallback_service no Router |
| Header de segurança em toda resposta | SetResponseHeaderLayer | HSTS, X-Content-Type-Options |
Regra de bolso: se o comportamento é transversal a todas as rotas e tem nome conhecido da indústria (CORS, timeout, rate limit), ele é camada — não código dentro do handler.
O que é tower-http (e onde ele encaixa)
Tower definiu o vocabulário: Service é uma função assíncrona que recebe uma requisição e devolve uma resposta; Layer envolve um Service e devolve outro Service com comportamento adicional. tower-http é o crate que usa esse modelo para entregar o middleware HTTP que quase todo serviço real precisa:
- trace — spans e eventos por requisição com
tracing; - timeout — orçamento de tempo por requisição;
- cors — política de compartilhamento entre origens;
- compression — gzip, brotli, deflate, zstd (por feature);
- request-id — gera e propaga identificador de correlação;
- limit — limite de tamanho do corpo da requisição;
- fs — arquivos estáticos via
ServeDireServeFile; - set-header / sensitive-headers — headers de segurança e sigilo;
- validate-request — exigir header ou token antes de chegar na rota.
Como Axum, Hyper e Tonic constroem sobre o mesmo trait Service, uma camada escrita para um serve aos outros. É por isso que entender tower-http vale em entrevistas de backend e plataforma: a pergunta real é “como você coloca política operacional em um serviço sem espalhar código pelos handlers”.
Setup: Cargo.toml e o ServiceBuilder base
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
tower = "0.5"
tower-http = { version = "0.6", features = [
"trace", "timeout", "cors", "compression-gzip",
"request-id", "limit", "fs", "set-header",
"sensitive-headers", "util",
] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["v4"] }
O stack de produção mínimo em um main.rs:
use std::time::Duration;
use axum::{routing::get, Router};
use tower_http::{
compression::CompressionLayer,
cors::CorsLayer,
limit::RequestBodyLimitLayer,
request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer},
timeout::TimeoutLayer,
trace::TraceLayer,
};
use tower::ServiceBuilder;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter("info,tower_http=debug")
.init();
let app = Router::new()
.route("/health", get(|| async { "ok" }))
.layer(
ServiceBuilder::new()
.set_x_request_id(MakeRequestUuid::default())
.propagate_x_request_id()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive()) // troque por política real
.layer(TimeoutLayer::new(Duration::from_secs(10)))
.layer(RequestBodyLimitLayer::new(1024 * 1024)) // 1 MiB
.layer(CompressionLayer::new()),
);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
Dez linhas de builder, e o serviço já tem request ID, tracing, CORS, timeout, limite de corpo e compressão. O resto do guia detalha cada peça e a ordem.
A ordem das layers importa (e é o erro mais comum)
Em ServiceBuilder, a primeira camada declarada é a mais externa: a requisição atravessa de cima para baixo e a resposta volta de baixo para cima. Ordem recomendada para APIs:
- Request ID (
set_x_request_id,propagate_x_request_id) — o mais externo, para que o ID exista em todos os logs e na resposta final. TraceLayer— logo depois, observando tudo que acontece dentro, inclusive o resultado do timeout.CorsLayer— externa o suficiente para que um preflightOPTIONSreceba resposta correta sem depender das camadas internas.TimeoutLayer— envolvendo a lógica de negócio; o tempo do handler é o que precisa de orçamento.RequestBodyLimitLayer— rejeitar payload grande antes de gastar tempo e memória.CompressionLayer— perto do handler, comprimindo a resposta final.
Sinais de ordem errada na prática:
- preflight CORS quebrando porque o timeout ou um middleware de auth responde antes do CORS;
- logs de tracing sem o request ID (ID declarado dentro do trace);
- timeout que nunca dispara porque foi colocado por dentro de uma camada que faz buffer do corpo.
Se um comportamento depende do outro, o provedor fica mais externo.
CORS de verdade (não permissive)
CorsLayer::permissive() serve para protótipo. Em produção, seja explícito:
use axum::http::{header, HeaderValue, Method};
let cors = CorsLayer::new()
.allow_origin("https://app.seudominio.com".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE]);
Para múltiplas origens, allow_origin aceita uma lista de HeaderValue (e há allow_origin_fn para lógica dinâmica, como domínios de preview de deploy). O erro clássico do frontend é ver a API responder, mas o navegador bloquear a leitura: isso é CORS na camada do navegador, resolvido com as respostas de preflight corretas — exatamente o que esta camada emite.
Timeout: orçamento de tempo por requisição
TimeoutLayer::new(Duration::from_secs(10)) aplica o mesmo orçamento a todas as rotas. Quando estoura, o cliente recebe 408 Request Timeout (nas versões recentes do tower-http). Três refinamentos úteis:
- timeout por rota: aplique
.layer(TimeoutLayer::new(...))em umRouterespecífico antes de montar o Router geral, com orçamento maior para rotas pesadas; - timeout de operação interna: para uma chamada a banco ou API externa, use
tokio::time::timeoutno handler — camada protege a requisição,tokioprotege a operação; - timeout + retry no cliente: o lado servidor limita o dano; para chamadas de saída, as políticas de resiliência com Tower entram em cena.
TraceLayer: observabilidade sem instrumentar cada handler
TraceLayer::new_for_http() cria um span por requisição e registra eventos em request_started/request_finished (nível DEBUG no crate tower_http) com método, caminho, status e latência. Dois ajustes que valem em produção:
use tower_http::trace::{DefaultMakeSpan, DefaultOnFailure, DefaultOnResponse, TraceLayer};
use tower_http::classify::ServerErrorsFailureClass;
let trace = TraceLayer::new_for_http()
.make_span_with(
DefaultMakeSpan::new()
.level(tracing::Level::INFO)
.include_headers(false),
)
.on_failure(DefaultOnFailure::new().level(tracing::Level::ERROR))
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO));
Combine com tracing e tracing-subscriber para JSON estruturado em produção, e com observabilidade e OpenTelemetry quando os spans precisarem cruzar serviços. Para rotas parametrizadas, capture o padrão da rota (ex.: /users/{id}) em vez do caminho bruto, senão a cardinalidade de métricas explode.
Corpo, compressão e headers sensíveis
Limite de corpo. RequestBodyLimitLayer::new(1024 * 1024) rejeita requisições acima de 1 MiB antes da leitura completa — barato e evita que um upload descuidado consuma memória. Para uploads legítimos, aplique a camada apenas no Router de upload.
Compressão. CompressionLayer com a feature compression-gzip (ou brotli/zstd) comprime respostas conforme o Accept-Encoding. Cuidado com SSE e streaming: compressão pode fazer buffer dos eventos e “congelar” o stream — exclua rotas de eventos da camada de compressão.
Headers sensíveis. Antes de logar requisições com include_headers(true) ou em proxies internas, esconda credenciais:
use axum::http::header::{AUTHORIZATION, COOKIE};
use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer;
let sensitive = SetSensitiveRequestHeadersLayer::new([
AUTHORIZATION,
COOKIE,
]);
Header de resposta fixo. Para HSTS ou X-Content-Type-Options:
use axum::http::{HeaderValue, header};
use tower_http::set_header::SetResponseHeaderLayer;
let xcto = SetResponseHeaderLayer::overriding(
header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
);
Arquivos estáticos: ServeDir no mesmo binário
Com a feature fs, o mesmo serviço Axum serve a API e o front:
use tower_http::services::{ServeDir, ServeFile};
let app = Router::new()
.route("/api/health", get(|| async { "ok" }))
.fallback_service(
ServeDir::new("dist")
.not_found_service(ServeFile::new("dist/index.html")),
);
O padrão acima serve qualquer asset de dist/ e devolve o index.html para caminhos desconhecidos — o comportamento que SPAs esperam para roteamento no cliente. Em deploy com Docker Compose, isso reduz o número de peças: um binário serve tudo.
Middleware customizado: from_fn antes de implementar Layer
Quando a lógica é específica da aplicação (checar JWT, injetar usuário no request extensions), Axum oferece middleware::from_fn, mais simples que implementar um Layer completo:
use axum::{extract::Request, middleware::Next, response::Response};
async fn log_metodo(req: Request, next: Next) -> Response {
let metodo = req.method().clone();
let resposta = next.run(req).await;
tracing::info!(%metodo, status = ?resposta.status(), "requisição processada");
resposta
}
// no Router: .layer(axum::middleware::from_fn(log_metodo))
Implemente um Layer de Tower (o modelo de tower) quando o middleware precisa ser reutilizado entre projetos ou entre Axum e Tonic; use from_fn para o resto. Para autenticação JWT completa há guia dedicado.
tower-http com Tonic (gRPC)
As camadas genéricas funcionam para qualquer Service, inclusive gRPC:
use tower_http::trace::TraceLayer;
let server = tonic::transport::Server::builder()
.layer(TraceLayer::new_for_grpc())
.add_service(meu_servico)
.serve(addr)
.await?;
TraceLayer::new_for_grpc() classifica erros pelo código de status gRPC (e não por código HTTP). Timeout, request ID e limite de concorrência aplicam-se igualmente — mesma política de produção para HTTP e gRPC, que é o argumento central do modelo Tower em carreira backend.
Erros comuns
- CORS
permissiveem produção — funciona, mas abre a API para qualquer origem; a política explícita é três linhas. OPTIONS404 sem CORS externo — o preflight morre antes de chegar à camada de CORS; revise a ordem do builder.- Timeout nunca dispara — camada colocada por dentro de outra que faz buffer, ou timeout no lugar errado do stack; a camada deve envolver o handler.
- Compressão congela SSE — exclua rotas de streaming da
CompressionLayer. - Features faltando no Cargo.toml —
CorsLayersem a featurecorsnão compila; cada grupo de camadas tem feature própria (compression-gzip,fs,limit…). - Cardinalidade de métricas estourando — logar
/users/42em vez do padrão/users/{id}cria uma série temporal por usuário.
Checklist de produção
set_x_request_id+propagate_x_request_idcomo camadas mais externas.TraceLayerlogo abaixo, com nível INFO por span em produção.CorsLayerexplícito — origens, métodos e headers autorizados.TimeoutLayercom orçamento realista (ex.: 10 s geral, 30 s em rotas pesadas).RequestBodyLimitLayercompatível com o maior payload legítimo.CompressionLayer— exceto em rotas de streaming.SetSensitiveRequestHeadersLayerparaAuthorizationeCookie.- Health check sem auth, sem compressão, com timeout curto.
- Features do Cargo.toml revisadas — só as usadas.
- Um teste de integração por camada: preflight, corpo acima do limite, requisição lenta.
tower-http na carreira
Em vagas Rust de backend e plataforma, raramente aparece “tower-http” no título — aparece o que ele entrega: middleware, observabilidade, resiliência, rate limit, APIs operáveis. Um repositório de portfólio com ServiceBuilder bem ordenado, spans com request ID e um README explicando a ordem das camadas demonstra maturidade de produção, o que pesa em processos de empresas que usam Rust. Para aprofundar o modelo por trás, leia a referência de Tower e o guia de Tokio.
Perguntas frequentes
As respostas curtas estão no front matter desta página e cobrem o que é tower-http, a diferença para Tower, CORS no Axum, timeout em rotas, a ordem das layers e o uso com Tonic. Para dúvidas de resiliência além do HTTP — retry, circuit breaker, load shedding — o guia é o de serviços resilientes com Tower, Axum e Tokio.
Conclusão
tower-http é o atalho honesto do ecossistema Rust: em vez de reimplementar CORS, timeout, tracing e compressão em cada serviço, você empilha camadas testadas sobre o mesmo modelo Service/Layer que sustenta Axum, Hyper e Tonic. Comece com o ServiceBuilder deste guia na ordem recomendada, troque permissive por uma política real de CORS e adicione o que faltar quando a operação pedir. O serviço resultante é menor, observável e pronto para as perguntas que uma entrevista de backend faz.