tower-http: CORS, Timeout e Trace no Axum | Rust Brasil

Guia prático de tower-http em Rust: CORS, TimeoutLayer, TraceLayer, compressão, request ID, limites de corpo, ServeDir e middleware de produção com Axum.

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çãoCamada do tower-httpObservação
Navegador bloqueando a API (preflight falha)CorsLayerorigens, métodos e headers explícitos
Handler pendurado travando conexõesTimeoutLayercliente recebe 408 quando estoura
Não saber latência nem status por rotaTraceLayer + tracingspans automáticos por requisição
Payload gigante derrubando o serviçoRequestBodyLimitLayerrejeite cedo, antes de ler o corpo
Respostas grandes sem compressãoCompressionLayercuidado com streaming/SSE
Correlacionar requisições entre serviçosSetRequestIdLayer + PropagateRequestIdLayerX-Request-Id ponta a ponta
Vazar headers sensíveis no logSetSensitiveRequestHeadersLayeresconde Authorization, Cookie
Servir SPA e assets no mesmo binárioServeDircom fallback_service no Router
Header de segurança em toda respostaSetResponseHeaderLayerHSTS, 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 ServeDir e ServeFile;
  • 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:

  1. 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.
  2. TraceLayer — logo depois, observando tudo que acontece dentro, inclusive o resultado do timeout.
  3. CorsLayer — externa o suficiente para que um preflight OPTIONS receba resposta correta sem depender das camadas internas.
  4. TimeoutLayer — envolvendo a lógica de negócio; o tempo do handler é o que precisa de orçamento.
  5. RequestBodyLimitLayer — rejeitar payload grande antes de gastar tempo e memória.
  6. 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 um Router especí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::timeout no handler — camada protege a requisição, tokio protege 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 permissive em produção — funciona, mas abre a API para qualquer origem; a política explícita é três linhas.
  • OPTIONS 404 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.tomlCorsLayer sem a feature cors não compila; cada grupo de camadas tem feature própria (compression-gzip, fs, limit…).
  • Cardinalidade de métricas estourando — logar /users/42 em vez do padrão /users/{id} cria uma série temporal por usuário.

Checklist de produção

  1. set_x_request_id + propagate_x_request_id como camadas mais externas.
  2. TraceLayer logo abaixo, com nível INFO por span em produção.
  3. CorsLayer explícito — origens, métodos e headers autorizados.
  4. TimeoutLayer com orçamento realista (ex.: 10 s geral, 30 s em rotas pesadas).
  5. RequestBodyLimitLayer compatível com o maior payload legítimo.
  6. CompressionLayer — exceto em rotas de streaming.
  7. SetSensitiveRequestHeadersLayer para Authorization e Cookie.
  8. Health check sem auth, sem compressão, com timeout curto.
  9. Features do Cargo.toml revisadas — só as usadas.
  10. 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.