Redirect HTTP em Zig: 301, 302, 307 e 308 com Segurança

Para tratar redirecionamentos HTTP em Zig com segurança, não basta repetir a requisição para o valor de Location. O cliente precisa distinguir 301, 302, 303, 307 e 308, resolver URLs relativas, limitar o número de saltos e decidir quais headers podem atravessar uma mudança de origem. Em especial, nunca reenvie Authorization ou cookies automaticamente para outro domínio.

A política recomendada é: siga redirects apenas para esquemas permitidos (http e https), use um teto de 5 a 10 saltos, preserve método e body somente em 307/308, transforme em GET no 303 e trate 301/302 com uma regra explícita. Esse desenho serve para CLIs, downloaders, webhooks, agentes, integrações REST e serviços que usam std.http.Client.

Como a API de std.http pode mudar antes do Zig 1.0, confira zig version e a documentação do release instalado. Os nomes exatos de fetch, Request, headers e URI podem variar; as regras do protocolo e os limites de segurança permanecem os mesmos.

Resposta rápida

StatusSignificadoMétodo na próxima requisição
301 Moved Permanentlyrecurso mudou de endereço de forma permanentepolítica explícita; clientes históricos podem trocar POST por GET
302 Foundredirect temporário, com semântica histórica ambíguapolítica explícita; não presuma preservação de POST
303 See Otherconsulte outro recursouse GET (ou HEAD se a requisição original era HEAD)
307 Temporary Redirectmesmo request em endereço temporáriopreserve método e body
308 Permanent Redirectmesmo request em endereço permanentepreserve método e body

Se você ainda está montando a chamada básica, comece pelo tutorial de std.http.Client, GET, POST e JSON. Para downloads reais, combine este guia com limite de tamanho, checksum e escrita atômica.

Por que redirects exigem uma política própria

No navegador, o redirecionamento parece automático. Em uma ferramenta de sistema, ele muda o destino de uma operação que pode carregar credenciais, body, idempotency key e dados privados. Considere esta sequência:

POST https://api.exemplo.com/faturas
Authorization: Bearer ...
Idempotency-Key: pedido-123

302 Location: https://outro-dominio.example/coletor

Se o cliente seguir cegamente, pode:

  • enviar o token para um domínio que não deveria recebê-lo;
  • repetir um POST e criar uma segunda cobrança;
  • transformar POST em GET sem que o chamador perceba;
  • entrar em loop entre duas URLs;
  • aceitar downgrade de HTTPS para HTTP;
  • alcançar um host interno por meio de um redirect externo;
  • baixar um body muito maior do que o limite definido para o recurso original.

Por isso, redirect não é apenas detalhe de transporte. É uma nova decisão de autorização e roteamento a cada salto.

Modelo de política em Zig

Separe a política do código que executa a requisição. Uma configuração pequena torna o comportamento revisável:

const RedirectPolicy = struct {
    max_hops: u8 = 8,
    allow_http: bool = true,
    allow_https: bool = true,
    allow_https_to_http: bool = false,
    preserve_sensitive_headers_same_origin_only: bool = true,
};

const RedirectError = error{
    MissingLocation,
    TooManyRedirects,
    UnsupportedScheme,
    HttpsDowngrade,
    InvalidLocation,
    BodyNotReplayable,
};

O cliente de alto nível pode retornar também a URL final e a quantidade de saltos:

const FetchOutcome = struct {
    status: std.http.Status,
    final_url: []const u8,
    redirect_count: u8,
    body: []u8,
};

Isso melhora logs e testes. O chamador sabe se pediu uma URL e terminou em outra, sem precisar deduzir a informação pelo conteúdo da resposta.

Algoritmo seguro, passo a passo

O loop conceitual é simples:

url_atual = url_inicial
método_atual = método_inicial
body_atual = body_inicial

para cada salto até max_hops:
    executar request com limites
    se não for redirect: retornar resposta e url_atual
    ler e validar Location
    resolver Location contra url_atual
    validar esquema, origem e downgrade
    decidir próximo método e próximo body pelo status
    remover headers que não podem atravessar a mudança
    url_atual = próxima_url

erro TooManyRedirects

Um esqueleto deliberadamente independente da assinatura exata de std.http.Client:

fn fetchFollowingRedirects(
    allocator: std.mem.Allocator,
    client: *std.http.Client,
    initial_url: []const u8,
    initial_method: std.http.Method,
    initial_body: ?[]const u8,
    policy: RedirectPolicy,
) !FetchOutcome {
    var current_url = try allocator.dupe(u8, initial_url);
    defer allocator.free(current_url);

    var method = initial_method;
    var body = initial_body;
    var hops: u8 = 0;

    while (true) {
        const response = try requestOnce(
            allocator,
            client,
            current_url,
            method,
            body,
        );
        defer response.deinit(allocator);

        if (!isRedirect(response.status)) {
            return try response.intoOutcome(allocator, current_url, hops);
        }

        if (hops >= policy.max_hops) return error.TooManyRedirects;

        const location = response.location orelse return error.MissingLocation;
        const next_url = try resolveLocation(allocator, current_url, location);
        errdefer allocator.free(next_url);

        try validateRedirectTarget(current_url, next_url, policy);
        applyMethodPolicy(response.status, &method, &body);

        allocator.free(current_url);
        current_url = next_url;
        hops += 1;
    }
}

As funções requestOnce e resolveLocation são os adaptadores que acompanham as mudanças da stdlib. Mantenha-as pequenas. A lógica de segurança — teto, status, origem e método — deve continuar testável sem rede real.

Como identificar um redirect

Considere apenas os códigos que o aplicativo suporta explicitamente:

fn isRedirect(status: std.http.Status) bool {
    return switch (status) {
        .moved_permanently,
        .found,
        .see_other,
        .temporary_redirect,
        .permanent_redirect => true,
        else => false,
    };
}

Não trate qualquer status 3xx como “tente outra URL”. 304 Not Modified, por exemplo, pertence ao fluxo de cache condicional e não carrega uma nova representação. O guia de ETag, If-None-Match e 304 em Zig mostra esse contrato separado.

Location pode ser relativo

O servidor não é obrigado a devolver uma URL completa. Todos estes valores são possíveis:

Location: https://cdn.exemplo.com/releases/app.tar.xz
Location: /login
Location: ../arquivo
Location: ?pagina=2

O destino deve ser resolvido em relação à URL atual. Para:

https://exemplo.com/api/v1/usuarios/42

um Location: ../43 aponta para um resultado diferente de uma concatenação ingênua. Não use current_url ++ location. Além de errar barras e segmentos .., a concatenação pode produzir interpretação incorreta de query e fragmento.

Use o parser/resolvedor de URI disponível no release do Zig ou encapsule uma biblioteca pequena e testada. Depois da resolução, normalize apenas o necessário para comparação de origem; não altere o path de forma que mude sua semântica no servidor.

301 e 302: a ambiguidade histórica

Os códigos 301 e 302 nasceram antes de clientes e servidores convergirem em comportamento consistente para métodos diferentes de GET. Navegadores historicamente transformaram muitos POSTs em GET ao seguir esses redirects. APIs que precisam preservar método passaram a usar 307 e 308; fluxos que desejam explicitamente um GET usam 303.

Uma política conservadora para cliente Zig é:

fn applyMethodPolicy(
    status: std.http.Status,
    method: *std.http.Method,
    body: *?[]const u8,
) !void {
    switch (status) {
        .see_other => {
            if (method.* != .HEAD) method.* = .GET;
            body.* = null;
        },
        .temporary_redirect, .permanent_redirect => {
            // Preserva método e body.
        },
        .moved_permanently, .found => {
            if (method.* == .GET or method.* == .HEAD) return;
            return error.AmbiguousRedirectMethod;
        },
        else => unreachable,
    }
}

Em um cliente compatível com navegador, você pode optar por transformar POST em GET em 301/302. Em um cliente de API, falhar de forma explícita costuma ser mais seguro. O importante é não deixar a escolha escondida no comportamento padrão de uma versão da biblioteca.

Body replayable: cuidado com streaming

Preservar POST em 307 ou 308 exige enviar o mesmo body novamente. Isso é fácil quando o payload está em memória:

const payload = "{\"evento\":\"build.finished\"}";

Mas pode ser impossível quando o body vem de:

  • stdin sem buffer;
  • arquivo já consumido e não reposicionado;
  • stream de compressão;
  • upload multipart gerado em tempo real;
  • pipe ou socket;
  • produtor que não é idempotente.

Modele essa capacidade:

const RequestBody = union(enum) {
    none,
    bytes: []const u8,
    replayable_file: struct { path: []const u8 },
    one_shot_stream: *anyopaque,
};

Ao receber 307/308, retorne BodyNotReplayable para um stream de uso único. Não tente reconstruir parcialmente o upload. Para arquivos, abra novamente e confirme tamanho/identidade quando isso fizer parte do contrato. O artigo de upload multipart e streaming aprofunda os limites desse caso.

Duas URLs pertencem à mesma origem quando esquema, host e porta efetiva são iguais. Compare pelo menos:

https://api.exemplo.com:443
https://api.exemplo.com

como equivalentes, mas trate estes destinos como origens diferentes:

https://api.exemplo.com
https://cdn.exemplo.com
http://api.exemplo.com
https://api.exemplo.com:8443

Ao mudar de origem, remova por padrão:

  • Authorization;
  • Proxy-Authorization;
  • Cookie;
  • headers de API key;
  • headers internos de tenant ou identidade;
  • assinatura HMAC calculada sobre URL, host ou body.

Host também deve ser recalculado para o novo destino. Uma assinatura de webhook ou API normalmente precisa ser refeita, não copiada. Veja webhooks com HMAC e idempotência para entender por que URL e body podem fazer parte da autenticação.

Mesmo na mesma origem, reavalie credenciais se o redirect atravessar uma fronteira de path que a aplicação considera sensível. A regra de origem é o mínimo técnico, não uma autorização universal.

Nunca aceite downgrade HTTPS para HTTP por acidente

Esta cadeia deve falhar por padrão:

https://api.exemplo.com/dados
  -> 302 Location: http://api.exemplo.com/dados

O downgrade remove a proteção de transporte e pode expor token e body. Se o programa realmente precisa falar HTTP em uma rede local, permita isso na URL inicial ou em uma allowlist explícita; não aceite que um servidor HTTPS decida silenciosamente reduzir a segurança.

A validação conceitual:

fn validateRedirectTarget(
    current_url: []const u8,
    next_url: []const u8,
    policy: RedirectPolicy,
) !void {
    const current = try parseUri(current_url);
    const next = try parseUri(next_url);

    if (!isAllowedScheme(next.scheme, policy)) {
        return error.UnsupportedScheme;
    }

    if (std.mem.eql(u8, current.scheme, "https") and
        std.mem.eql(u8, next.scheme, "http") and
        !policy.allow_https_to_http)
    {
        return error.HttpsDowngrade;
    }
}

Para certificados, CA bundle, SNI e mTLS, consulte o guia de TLS e HTTPS em Zig.

Redirects e SSRF

Se a URL inicial ou o Location puder ser influenciado por usuário, um redirect pode contornar uma validação feita apenas no primeiro host:

https://site-publico.example/atalho
  -> http://127.0.0.1:8080/admin

ou:

https://site-publico.example/atalho
  -> http://169.254.169.254/...

Para clientes expostos a input não confiável, valide cada destino após resolver DNS e antes de conectar. Bloqueie loopback, link-local, redes privadas e endereços reservados quando o produto não precisa acessá-los. Considere também DNS rebinding: validar apenas o texto do hostname não basta em cenários de alto risco.

Uma allowlist de hosts é mais simples e forte quando a integração conhece os domínios válidos. Por exemplo, um updater oficial pode aceitar somente ziglang.org e o host de downloads esperado, em vez de tentar classificar toda a internet.

Limites devem valer em todos os saltos

Cada request da cadeia precisa respeitar:

  • timeout de conexão;
  • timeout de leitura;
  • limite de headers;
  • limite de body;
  • política de TLS;
  • cancelamento do chamador;
  • orçamento total da operação.

Não reinicie um timeout global de 30 segundos a cada redirect se isso permitir 8 saltos de 30 segundos. Use um deadline total e calcule o tempo restante antes de cada request. Combine com timeout, retry e circuit breaker: redirects não devem multiplicar retries sem teto.

Também não grave bodies intermediários enormes. Em geral, respostas de redirect têm body irrelevante; descarte-o com um limite pequeno antes de reutilizar ou fechar a conexão, conforme a API do cliente exigir.

Detecção de loop

O teto de saltos já impede loop infinito, mas registrar URLs visitadas produz um erro melhor:

A -> B -> C -> B

Use um conjunto de URLs normalizadas para detectar repetição. Não inclua credenciais de userinfo em logs e redija query strings quando elas puderem carregar token. Um log seguro pode registrar:

redirect_loop host=api.exemplo.com path=/oauth/callback hops=3

em vez da URL completa com ?code=....

Cache de 301 e 308

301 e 308 comunicam mudança permanente e podem ser cacheados. Em uma CLI de conteúdo público, isso reduz uma chamada futura. Em autenticação, deploy e download de artefato, persistir o destino para sempre pode causar comportamento difícil de desfazer.

Se você guardar redirects permanentes:

  1. salve origem, destino e instante da decisão;
  2. aplique TTL ou versão do cache;
  3. valide novamente depois do prazo;
  4. ofereça limpeza do cache;
  5. não persista redirect que atravessou uma política excepcional;
  6. não misture esse cache com o body do recurso.

Um redirect permanente também pode mudar novamente. “Permanente” é intenção do servidor, não garantia matemática.

Como testar sem depender da internet

Suba um servidor local controlado e cubra pelo menos:

  1. 302 de GET para caminho relativo;
  2. 303 transformando POST em GET e removendo o body;
  3. 307 preservando POST e body;
  4. 308 preservando método em mudança permanente;
  5. Location ausente;
  6. mais redirects que max_hops;
  7. loop A -> B -> A;
  8. HTTPS para HTTP rejeitado;
  9. mudança de host removendo Authorization;
  10. mesma origem preservando headers permitidos;
  11. body de uso único rejeitado em 307;
  12. destino para IP privado bloqueado, quando a política SSRF estiver ativa;
  13. budget total expirando no meio da cadeia;
  14. status 304 não sendo confundido com redirect.

O servidor fake deve registrar método, path, headers e body recebidos em cada salto. Testar somente a resposta final não prova que o token foi removido ou que o POST foi preservado corretamente.

Observabilidade útil

Em produção, registre métricas como:

  • http_client_redirects_total{status="307",host="..."};
  • quantidade de operações que terminaram após redirect;
  • redirects bloqueados por downgrade;
  • redirects bloqueados por mudança de origem;
  • TooManyRedirects e loops;
  • latência total, não apenas do último salto;
  • método original e método final, sem payload.

Não use a URL completa como label de Prometheus: cardinalidade explode e query strings podem conter dados sensíveis. Prefira nome da dependência e host normalizado. Para montar a camada completa, leia observabilidade com logs e Prometheus em Zig.

Checklist de produção

  • Apenas 301, 302, 303, 307 e 308 entram na política de redirect.
  • 304 Not Modified segue o fluxo separado de cache.
  • Existe um limite explícito de 5 a 10 saltos.
  • Location relativo é resolvido contra a URL atual.
  • Esquemas diferentes de HTTP/HTTPS são rejeitados.
  • Downgrade de HTTPS para HTTP é bloqueado por padrão.
  • Mudança de origem remove Authorization, cookies e API keys.
  • 303 troca para GET; 307 e 308 preservam método e body.
  • 301 e 302 com POST têm comportamento documentado, não implícito.
  • Body em streaming só é repetido se for realmente replayable.
  • Cada salto respeita limite de body, timeout e deadline total.
  • Destinos são revalidados contra SSRF quando a URL não é confiável.
  • Loops geram erro observável sem vazar query ou credencial.
  • Testes inspecionam método e headers de cada request intermediário.

Conclusão

Redirecionamento HTTP em Zig deve ser tratado como uma cadeia de novas decisões, não como “pegar Location e tentar de novo”. O cliente precisa saber quando preservar o método, quando descartar o body, quais credenciais remover e quais destinos jamais aceitar.

A implementação robusta cabe em uma camada pequena: requestOnce, resolução de URI, política de método, comparação de origem e um loop com limite. Essa separação combina com Zig porque torna cada estado visível e testável. Você pode atualizar o adaptador de std.http.Client entre releases sem reescrever as regras que protegem tokens, uploads e rede interna.

Para continuar, conecte este fluxo ao tutorial de cliente HTTP em Zig, ao guia de cache com ETag e ao padrão de circuit breaker, timeout e retry. Juntos, eles formam um cliente HTTP pequeno, previsível e adequado para produção.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.