Hyper: Biblioteca HTTP de Baixo Nível para Rust

Guia completo do Hyper em Rust: HTTP/1 e HTTP/2, construção de servidores e clientes customizados, connection pooling, integração com Axum e Reqwest, e exemplos práticos.

Introdução

O Hyper é a biblioteca HTTP de baixo nível mais importante do ecossistema Rust. Ele implementa o protocolo HTTP/1.1 e HTTP/2 de forma correta, segura e extremamente performática. Praticamente todo o ecossistema web do Rust é construído sobre o Hyper: o Axum usa Hyper como servidor, o Reqwest usa Hyper como cliente, e o Tonic usa Hyper para transportar gRPC.

Diferente de frameworks de alto nível como Axum ou Actix Web, o Hyper expõe os detalhes do protocolo HTTP, dando a você controle total sobre como as requisições e respostas são processadas. Isso o torna ideal para construir frameworks customizados, proxies, load balancers e qualquer componente de infraestrutura HTTP.

Por que entender o Hyper?

  • Fundação do ecossistema: Axum, Reqwest e Tonic são construídos sobre ele
  • Performance extrema: um dos servidores HTTP mais rápidos em qualquer linguagem
  • Controle total: acesso a cada detalhe do protocolo HTTP
  • HTTP/2 nativo: suporte completo a HTTP/2, incluindo multiplexação
  • Streaming: processamento de corpos de requisição/resposta como streams
  • Modular: pode ser usado como servidor, cliente, ou ambos

Instalação

Adicione o Hyper ao seu Cargo.toml:

[dependencies]
hyper = { version = "1", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }
http-body-util = "0.1"
tokio = { version = "1", features = ["full"] }
bytes = "1"

As features do Hyper são granulares, permitindo incluir apenas o que você precisa:

[dependencies]
# Apenas servidor HTTP/1
hyper = { version = "1", features = ["http1", "server"] }

# Apenas cliente HTTP/2
hyper = { version = "1", features = ["http2", "client"] }

# Tudo incluído
hyper = { version = "1", features = ["full"] }

O pacote hyper-util fornece utilitários adicionais como TokioIo para integrar com Tokio e TokioExecutor para execução de tasks.

Uso Básico

Servidor HTTP simples

O exemplo mais básico de um servidor HTTP com Hyper:

use std::convert::Infallible;
use std::net::SocketAddr;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;

// Handler que processa cada requisição
async fn handler(
    req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
    let resposta = match (req.method(), req.uri().path()) {
        (&hyper::Method::GET, "/") => {
            Response::new(Full::new(Bytes::from("Olá, mundo!")))
        }
        (&hyper::Method::GET, "/saude") => {
            Response::new(Full::new(Bytes::from(r#"{"status": "ok"}"#)))
        }
        _ => {
            let mut resp = Response::new(Full::new(Bytes::from("Não encontrado")));
            *resp.status_mut() = StatusCode::NOT_FOUND;
            resp
        }
    };

    Ok(resposta)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    println!("Servidor rodando em http://{}", addr);

    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);

        tokio::task::spawn(async move {
            if let Err(err) = http1::Builder::new()
                .serve_connection(io, service_fn(handler))
                .await
            {
                eprintln!("Erro ao servir conexão: {:?}", err);
            }
        });
    }
}

Servidor com estado compartilhado

use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;

// Estado compartilhado entre todas as conexões
struct AppState {
    contador_requisicoes: AtomicU64,
    nome_app: String,
}

async fn handler(
    state: Arc<AppState>,
    _req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
    let contagem = state.contador_requisicoes.fetch_add(1, Ordering::Relaxed) + 1;

    let corpo = format!(
        "App: {} | Requisição #{}\n",
        state.nome_app, contagem
    );

    Ok(Response::new(Full::new(Bytes::from(corpo))))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let state = Arc::new(AppState {
        contador_requisicoes: AtomicU64::new(0),
        nome_app: "Meu Servidor Hyper".to_string(),
    });

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;
    println!("Servidor rodando em http://{}", addr);

    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let state = state.clone();

        tokio::task::spawn(async move {
            let service = service_fn(move |req| {
                let state = state.clone();
                async move { handler(state, req).await }
            });

            if let Err(err) = http1::Builder::new()
                .serve_connection(io, service)
                .await
            {
                eprintln!("Erro: {:?}", err);
            }
        });
    }
}

Roteamento manual

use std::convert::Infallible;
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use hyper::{Method, Request, Response, StatusCode};

async fn roteador(
    req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
    match (req.method(), req.uri().path()) {
        // GET /
        (&Method::GET, "/") => ok_json(r#"{"mensagem": "Bem-vindo à API"}"#),

        // GET /usuarios
        (&Method::GET, "/usuarios") => {
            ok_json(r#"[{"id": 1, "nome": "Maria"}, {"id": 2, "nome": "João"}]"#)
        }

        // POST /usuarios
        (&Method::POST, "/usuarios") => {
            // Ler o corpo da requisição
            let corpo = req.collect().await
                .map(|c| c.to_bytes())
                .unwrap_or_default();

            let texto = String::from_utf8_lossy(&corpo);
            let resposta = format!(r#"{{"criado": true, "dados": {}}}"#, texto);

            let mut resp = Response::new(Full::new(Bytes::from(resposta)));
            *resp.status_mut() = StatusCode::CREATED;
            resp.headers_mut().insert(
                hyper::header::CONTENT_TYPE,
                "application/json".parse().unwrap(),
            );
            Ok(resp)
        }

        // GET /usuarios/:id (roteamento simples por prefixo)
        (&Method::GET, path) if path.starts_with("/usuarios/") => {
            let id = &path["/usuarios/".len()..];
            ok_json(&format!(r#"{{"id": {}, "nome": "Usuário {}"}}"#, id, id))
        }

        // 404 para todas as outras rotas
        _ => {
            let mut resp = Response::new(Full::new(Bytes::from(
                r#"{"erro": "Rota não encontrada"}"#,
            )));
            *resp.status_mut() = StatusCode::NOT_FOUND;
            Ok(resp)
        }
    }
}

fn ok_json(corpo: &str) -> Result<Response<Full<Bytes>>, Infallible> {
    let mut resp = Response::new(Full::new(Bytes::from(corpo.to_string())));
    resp.headers_mut().insert(
        hyper::header::CONTENT_TYPE,
        "application/json".parse().unwrap(),
    );
    Ok(resp)
}

Recursos Avançados

Servidor HTTP/2

use std::net::SocketAddr;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http2;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::TcpListener;

async fn handler(
    _req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, std::convert::Infallible> {
    Ok(Response::new(Full::new(Bytes::from("HTTP/2 funcionando!"))))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    println!("Servidor HTTP/2 em http://{}", addr);

    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);

        tokio::task::spawn(async move {
            if let Err(err) = http2::Builder::new(TokioExecutor::new())
                .serve_connection(io, service_fn(handler))
                .await
            {
                eprintln!("Erro HTTP/2: {:?}", err);
            }
        });
    }
}

Servidor que suporta HTTP/1 e HTTP/2 simultaneamente

use std::net::SocketAddr;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto;
use tokio::net::TcpListener;

async fn handler(
    req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, std::convert::Infallible> {
    let versao = format!("{:?}", req.version());
    let corpo = format!("Versão HTTP: {}", versao);
    Ok(Response::new(Full::new(Bytes::from(corpo))))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    println!("Servidor HTTP/1+2 em http://{}", addr);

    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);

        tokio::task::spawn(async move {
            if let Err(err) = auto::Builder::new(TokioExecutor::new())
                .serve_connection(io, service_fn(handler))
                .await
            {
                eprintln!("Erro: {:?}", err);
            }
        });
    }
}

Cliente HTTP com Hyper

use http_body_util::{BodyExt, Empty};
use hyper::body::Bytes;
use hyper::Request;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let client = Client::builder(TokioExecutor::new())
        .build_http();

    // Construir requisição manualmente
    let req = Request::builder()
        .method("GET")
        .uri("http://httpbin.org/get")
        .header("User-Agent", "hyper-client/1.0")
        .body(Empty::<Bytes>::new())?;

    let resp = client.request(req).await?;

    println!("Status: {}", resp.status());
    println!("Headers: {:#?}", resp.headers());

    // Ler o corpo completo
    let corpo = resp.into_body().collect().await?.to_bytes();
    println!("Corpo: {}", String::from_utf8_lossy(&corpo));

    Ok(())
}

Streaming de corpo de resposta

use std::convert::Infallible;

use bytes::Bytes;
use http_body_util::StreamBody;
use hyper::body::Frame;
use hyper::{Request, Response};
use tokio_stream::StreamExt;

async fn streaming_handler(
    _req: Request<hyper::body::Incoming>,
) -> Result<Response<StreamBody<impl tokio_stream::Stream<Item = Result<Frame<Bytes>, Infallible>>>>, Infallible> {
    // Criar um stream que envia dados gradualmente
    let stream = tokio_stream::iter(0..10).map(|i| {
        let chunk = format!("Chunk {}: dados do servidor\n", i);
        Ok(Frame::data(Bytes::from(chunk)))
    });

    let body = StreamBody::new(stream);
    Ok(Response::new(body))
}

Middleware manual (wrapping services)

use std::convert::Infallible;
use std::time::Instant;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::{Request, Response};

// Wrapper que adiciona logging
async fn com_logging(
    req: Request<hyper::body::Incoming>,
    handler: impl Fn(Request<hyper::body::Incoming>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Response<Full<Bytes>>, Infallible>> + Send>>,
) -> Result<Response<Full<Bytes>>, Infallible> {
    let metodo = req.method().clone();
    let caminho = req.uri().path().to_string();
    let inicio = Instant::now();

    let resposta = handler(req).await;

    let duracao = inicio.elapsed();
    if let Ok(ref resp) = resposta {
        println!(
            "{} {} -> {} ({:?})",
            metodo,
            caminho,
            resp.status(),
            duracao
        );
    }

    resposta
}

Graceful shutdown

use std::net::SocketAddr;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio::signal;

async fn handler(
    _req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, std::convert::Infallible> {
    Ok(Response::new(Full::new(Bytes::from("Servidor ativo!"))))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;
    println!("Servidor em http://{}", addr);

    // Canal para sinalizar shutdown
    let (tx_shutdown, _) = tokio::sync::broadcast::channel::<()>(1);

    loop {
        tokio::select! {
            resultado = listener.accept() => {
                let (stream, _) = resultado?;
                let io = TokioIo::new(stream);
                let mut rx = tx_shutdown.subscribe();

                tokio::task::spawn(async move {
                    let conn = http1::Builder::new()
                        .serve_connection(io, service_fn(handler));

                    tokio::pin!(conn);

                    tokio::select! {
                        resultado = &mut conn => {
                            if let Err(err) = resultado {
                                eprintln!("Erro: {:?}", err);
                            }
                        }
                        _ = rx.recv() => {
                            println!("Encerrando conexão graciosamente...");
                            conn.as_mut().graceful_shutdown();
                            // Aguardar conclusão das requisições em andamento
                            if let Err(err) = conn.await {
                                eprintln!("Erro no shutdown: {:?}", err);
                            }
                        }
                    }
                });
            }
            _ = signal::ctrl_c() => {
                println!("\nRecebido Ctrl+C, encerrando...");
                let _ = tx_shutdown.send(());
                break;
            }
        }
    }

    println!("Servidor encerrado.");
    Ok(())
}

Boas Práticas

1. Use Hyper apenas quando necessário

Na maioria dos casos, frameworks de alto nível como Axum ou Actix Web são mais apropriados. Use Hyper diretamente quando:

  • Estiver construindo um framework web
  • Precisar de controle total sobre o protocolo HTTP
  • Estiver construindo um proxy ou load balancer
  • Precisar de performance extrema e quiser eliminar overhead

2. Sempre trate erros de conexão

use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;

async fn aceitar_conexoes(listener: TcpListener) {
    loop {
        match listener.accept().await {
            Ok((stream, addr)) => {
                println!("Nova conexão de: {}", addr);
                let io = TokioIo::new(stream);

                tokio::task::spawn(async move {
                    let resultado = http1::Builder::new()
                        .serve_connection(io, service_fn(|_req| async {
                            Ok::<_, std::convert::Infallible>(
                                hyper::Response::new(
                                    http_body_util::Full::new(
                                        hyper::body::Bytes::from("OK")
                                    )
                                )
                            )
                        }))
                        .await;

                    if let Err(err) = resultado {
                        // Diferenciar tipos de erro
                        if err.is_incomplete_message() {
                            // Cliente desconectou - normal
                        } else if err.is_parse() {
                            eprintln!("Erro de parse HTTP de {}: {}", addr, err);
                        } else {
                            eprintln!("Erro de conexão de {}: {}", addr, err);
                        }
                    }
                });
            }
            Err(e) => {
                eprintln!("Erro ao aceitar conexão: {}", e);
                tokio::time::sleep(std::time::Duration::from_millis(100)).await;
            }
        }
    }
}

3. Configure limites de conexão

use hyper::server::conn::http1;

fn configurar_http1() -> http1::Builder {
    let mut builder = http1::Builder::new();

    // Limitar tamanho do cabeçalho (padrão: ~400KB)
    builder.max_buf_size(8 * 1024); // 8KB

    // Habilitar keep-alive
    builder.keep_alive(true);

    // Habilitar pipeline HTTP/1.1
    builder.pipeline_flush(true);

    builder
}

4. Prefira tipos concretos a trait objects

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::Response;

// Bom: tipo concreto, sem alocação dinâmica
fn resposta_rapida() -> Response<Full<Bytes>> {
    Response::new(Full::new(Bytes::from("Rápido!")))
}

// Evitar em hot paths: BoxBody usa alocação dinâmica
// use http_body_util::combinators::BoxBody;
// fn resposta_lenta() -> Response<BoxBody<Bytes, hyper::Error>> { ... }

5. Use tower::Service para composição

// O Hyper se integra com Tower para middleware composável
// Veja a página sobre Tower para detalhes

use tower::ServiceBuilder;
use tower::timeout::TimeoutLayer;
use std::time::Duration;

// Exemplo conceitual de composição com Tower
fn criar_service() {
    let _service = ServiceBuilder::new()
        .layer(TimeoutLayer::new(Duration::from_secs(30)))
        // .layer(RateLimitLayer::new(100, Duration::from_secs(1)))
        // .service(meu_handler)
        ;
}

Exemplos Práticos

Servidor HTTP completo com roteamento e JSON

use std::collections::HashMap;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};

use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use hyper::header::CONTENT_TYPE;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Nota {
    id: u64,
    titulo: String,
    conteudo: String,
}

struct AppState {
    notas: Mutex<HashMap<u64, Nota>>,
    proximo_id: Mutex<u64>,
}

impl AppState {
    fn new() -> Self {
        Self {
            notas: Mutex::new(HashMap::new()),
            proximo_id: Mutex::new(1),
        }
    }
}

fn json_response(status: StatusCode, corpo: &str) -> Response<Full<Bytes>> {
    let mut resp = Response::new(Full::new(Bytes::from(corpo.to_string())));
    *resp.status_mut() = status;
    resp.headers_mut().insert(CONTENT_TYPE, "application/json".parse().unwrap());
    resp
}

async fn roteador(
    state: Arc<AppState>,
    req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
    let metodo = req.method().clone();
    let caminho = req.uri().path().to_string();

    let resposta = match (metodo, caminho.as_str()) {
        // Listar todas as notas
        (Method::GET, "/notas") => {
            let notas = state.notas.lock().unwrap();
            let lista: Vec<&Nota> = notas.values().collect();
            let json = serde_json::to_string(&lista).unwrap();
            json_response(StatusCode::OK, &json)
        }

        // Criar nota
        (Method::POST, "/notas") => {
            let corpo = req.collect().await.unwrap().to_bytes();
            match serde_json::from_slice::<serde_json::Value>(&corpo) {
                Ok(dados) => {
                    let mut proximo_id = state.proximo_id.lock().unwrap();
                    let id = *proximo_id;
                    *proximo_id += 1;

                    let nota = Nota {
                        id,
                        titulo: dados["titulo"]
                            .as_str()
                            .unwrap_or("Sem título")
                            .to_string(),
                        conteudo: dados["conteudo"]
                            .as_str()
                            .unwrap_or("")
                            .to_string(),
                    };

                    let json = serde_json::to_string(&nota).unwrap();
                    state.notas.lock().unwrap().insert(id, nota);
                    json_response(StatusCode::CREATED, &json)
                }
                Err(_) => json_response(
                    StatusCode::BAD_REQUEST,
                    r#"{"erro": "JSON inválido"}"#,
                ),
            }
        }

        // Obter nota por ID
        (Method::GET, path) if path.starts_with("/notas/") => {
            let id_str = &path["/notas/".len()..];
            match id_str.parse::<u64>() {
                Ok(id) => {
                    let notas = state.notas.lock().unwrap();
                    match notas.get(&id) {
                        Some(nota) => {
                            let json = serde_json::to_string(nota).unwrap();
                            json_response(StatusCode::OK, &json)
                        }
                        None => json_response(
                            StatusCode::NOT_FOUND,
                            r#"{"erro": "Nota não encontrada"}"#,
                        ),
                    }
                }
                Err(_) => json_response(
                    StatusCode::BAD_REQUEST,
                    r#"{"erro": "ID inválido"}"#,
                ),
            }
        }

        // Deletar nota
        (Method::DELETE, path) if path.starts_with("/notas/") => {
            let id_str = &path["/notas/".len()..];
            match id_str.parse::<u64>() {
                Ok(id) => {
                    let mut notas = state.notas.lock().unwrap();
                    if notas.remove(&id).is_some() {
                        json_response(StatusCode::OK, r#"{"removida": true}"#)
                    } else {
                        json_response(
                            StatusCode::NOT_FOUND,
                            r#"{"erro": "Nota não encontrada"}"#,
                        )
                    }
                }
                Err(_) => json_response(
                    StatusCode::BAD_REQUEST,
                    r#"{"erro": "ID inválido"}"#,
                ),
            }
        }

        // Rota padrão
        _ => json_response(
            StatusCode::NOT_FOUND,
            r#"{"erro": "Rota não encontrada"}"#,
        ),
    };

    Ok(resposta)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let state = Arc::new(AppState::new());
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = TcpListener::bind(addr).await?;

    println!("API de Notas rodando em http://{}", addr);
    println!("Rotas:");
    println!("  GET    /notas      - Listar notas");
    println!("  POST   /notas      - Criar nota");
    println!("  GET    /notas/:id  - Obter nota");
    println!("  DELETE /notas/:id  - Deletar nota");

    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let state = state.clone();

        tokio::task::spawn(async move {
            let service = service_fn(move |req| {
                let state = state.clone();
                async move { roteador(state, req).await }
            });

            if let Err(err) = http1::Builder::new()
                .serve_connection(io, service)
                .await
            {
                eprintln!("Erro de conexão: {:?}", err);
            }
        });
    }
}

Proxy reverso simples

use std::convert::Infallible;
use std::net::SocketAddr;

use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::client::legacy::Client;
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::TcpListener;

async fn proxy(
    client: Client<hyper_util::client::legacy::connect::HttpConnector, Full<Bytes>>,
    destino: String,
    req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
    let caminho = req.uri().path().to_string();
    let query = req.uri().query().map(|q| format!("?{}", q)).unwrap_or_default();
    let uri_destino = format!("{}{}{}", destino, caminho, query);

    println!("Proxy: {} -> {}", req.uri(), uri_destino);

    // Construir nova requisição para o destino
    let proxy_req = Request::builder()
        .method(req.method())
        .uri(&uri_destino)
        .body(Full::new(Bytes::new()))
        .unwrap();

    match client.request(proxy_req).await {
        Ok(resp) => {
            let status = resp.status();
            let corpo = resp.into_body().collect().await
                .map(|c| c.to_bytes())
                .unwrap_or_default();

            let mut resposta = Response::new(Full::new(corpo));
            *resposta.status_mut() = status;
            Ok(resposta)
        }
        Err(err) => {
            eprintln!("Erro no proxy: {}", err);
            let mut resp = Response::new(Full::new(Bytes::from("Erro no proxy")));
            *resp.status_mut() = StatusCode::BAD_GATEWAY;
            Ok(resp)
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let destino = "http://httpbin.org".to_string();
    let client = Client::builder(TokioExecutor::new()).build_http();

    let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
    let listener = TcpListener::bind(addr).await?;
    println!("Proxy rodando em http://{} -> {}", addr, destino);

    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let client = client.clone();
        let destino = destino.clone();

        tokio::task::spawn(async move {
            let service = service_fn(move |req| {
                let client = client.clone();
                let destino = destino.clone();
                async move { proxy(client, destino, req).await }
            });

            if let Err(err) = http1::Builder::new()
                .serve_connection(io, service)
                .await
            {
                eprintln!("Erro: {:?}", err);
            }
        });
    }
}

Comparação com Alternativas

CaracterísticaHyperAxumActix WebWarp
NívelBaixo nívelAlto nívelAlto nívelAlto nível
RoteamentoManualEmbutidoEmbutidoEmbutido
MiddlewareManual/TowerTowerPróprioFilters
HTTP/2NativoVia HyperNativoVia Hyper
ErgonomiaBaixaAltaAltaMédia
PerformanceMáximaExcelenteExcelenteExcelente
Casos de usoInfra/frameworksAPIs/web appsAPIs/web appsAPIs/web apps
  • Hyper vs Axum: Axum é construído sobre Hyper e adiciona roteamento, extratores e middleware. Use Axum para aplicações web e Hyper para componentes de infraestrutura.
  • Hyper vs Actix Web: Actix Web usa seu próprio runtime e modelo de atores. Hyper é mais flexível e integra melhor com o ecossistema Tokio.
  • Hyper vs Warp: Warp também usa Hyper internamente, mas com uma API baseada em filtros composáveis. Warp tem menos manutenção ativa que Axum.

Conclusão

O Hyper é a fundação do ecossistema HTTP em Rust. Mesmo que você nunca o use diretamente, entender como ele funciona vai ajudá-lo a debugar problemas, otimizar performance e tomar decisões arquiteturais melhores ao usar frameworks como Axum ou Reqwest.

Para aplicações web típicas, prefira usar Axum (que é construído sobre Hyper). Reserve o uso direto do Hyper para cenários que exigem controle total do protocolo HTTP, como proxies, load balancers e frameworks customizados.

Próximos passos

  • Aprenda Axum para construir APIs web de forma ergonômica sobre o Hyper
  • Explore Tower para adicionar middleware composável ao Hyper
  • Estude Reqwest para o lado do cliente HTTP (também construído sobre Hyper)
  • Veja Tonic para gRPC, que também é construído sobre Hyper