---
title: "Redirect HTTP em Zig: 301, 302, 307 e 308 com Segurança"
url: "https://ziglang.com.br/artigos/zig-http-redirecionamentos-301-302-307-308/"
markdown_url: "https://ziglang.com.br/artigos/zig-http-redirecionamentos-301-302-307-308.MD"
description: "Como tratar redirects HTTP em Zig: códigos 301, 302, 303, 307 e 308, header Location, limite de saltos, URLs relativas, POST, Authorization e testes."
date: "2026-08-13"
author: ""
---

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

Como tratar redirects HTTP em Zig: códigos 301, 302, 303, 307 e 308, header Location, limite de saltos, URLs relativas, POST, Authorization e testes.


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

| Status | Significado | Método na próxima requisição |
|---|---|---|
| `301 Moved Permanently` | recurso mudou de endereço de forma permanente | política explícita; clientes históricos podem trocar POST por GET |
| `302 Found` | redirect temporário, com semântica histórica ambígua | política explícita; não presuma preservação de POST |
| `303 See Other` | consulte outro recurso | use `GET` (ou `HEAD` se a requisição original era `HEAD`) |
| `307 Temporary Redirect` | mesmo request em endereço temporário | preserve método e body |
| `308 Permanent Redirect` | mesmo request em endereço permanente | preserve método e body |

Se você ainda está montando a chamada básica, comece pelo tutorial de [`std.http.Client`, GET, POST e JSON](/tutoriais/zig-http-client/). Para downloads reais, combine este guia com [limite de tamanho, checksum e escrita atômica](/artigos/zig-download-arquivos-http-checksum/).

## 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:

```text
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:

```zig
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:

```zig
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:

```text
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`:

```zig
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:

```zig
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](/artigos/zig-http-cache-etag-if-none-match/) 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:

```http
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:

```text
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 é:

```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:

```zig
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:

```zig
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](/artigos/zig-http-multipart-upload-arquivos/) aprofunda os limites desse caso.

## Authorization, Cookie e mudança de origem

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

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

como equivalentes, mas trate estes destinos como origens diferentes:

```text
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](/artigos/zig-webhooks-hmac-idempotencia/) 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:

```text
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:

```zig
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](/artigos/zig-tls-https-certificados-mtls/).

## 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:

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

ou:

```text
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](/artigos/zig-circuit-breaker-timeout-retry/): 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:

```text
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:

```text
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](/artigos/zig-observabilidade-logs-prometheus/).

## 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](/tutoriais/zig-http-client/), ao guia de [cache com ETag](/artigos/zig-http-cache-etag-if-none-match/) e ao padrão de [circuit breaker, timeout e retry](/artigos/zig-circuit-breaker-timeout-retry/). Juntos, eles formam um cliente HTTP pequeno, previsível e adequado para produção.
