---
title: "CORS em Zig: Preflight, Headers e Segurança para APIs HTTP"
url: "https://ziglang.com.br/artigos/zig-cors-api-http/"
markdown_url: "https://ziglang.com.br/artigos/zig-cors-api-http.MD"
description: "Como configurar CORS em uma API Zig: Access-Control-Allow-Origin, preflight OPTIONS, credenciais, cache, headers permitidos, testes e erros comuns."
date: "2026-08-14"
author: ""
---

# CORS em Zig: Preflight, Headers e Segurança para APIs HTTP

Como configurar CORS em uma API Zig: Access-Control-Allow-Origin, preflight OPTIONS, credenciais, cache, headers permitidos, testes e erros comuns.


Para configurar **CORS em uma API Zig**, trate o header `Origin` como entrada não confiável, compare-o com uma lista explícita de origens permitidas e devolva os headers CORS somente quando a política autorizar. Para requisições de *preflight*, responda a `OPTIONS` antes do handler principal e declare apenas os métodos e headers que a rota realmente aceita.

A regra mais importante é simples: **não use `Access-Control-Allow-Origin: *` por conveniência em uma API autenticada**. Se o frontend envia cookies ou outra credencial gerenciada pelo navegador, a resposta precisa indicar uma origem específica e pode usar `Access-Control-Allow-Credentials: true`. CORS não substitui autenticação, autorização, CSRF, validação de entrada nem rate limiting.

Como a API de `std.http.Server` ainda pode mudar antes do Zig 1.0, os exemplos abaixo concentram a lógica em funções pequenas e adaptáveis. Confira `zig version` e a documentação da versão instalada para ajustar a escrita dos headers e da resposta; a política HTTP permanece a mesma.

## Resposta rápida: política recomendada

| Cenário | `Allow-Origin` | Credenciais | Observação |
|---|---|---:|---|
| API pública sem cookies | `*` pode ser aceitável | não | ainda exige limites e autenticação quando aplicável |
| SPA própria em produção | origem exata, como `https://app.exemplo.com.br` | conforme o login | mantenha allowlist explícita |
| frontend local de desenvolvimento | `http://localhost:3000` ou porta usada | conforme o login | não leve a liberação ampla para produção |
| vários domínios controlados | refletir somente origens presentes na allowlist | possível | envie `Vary: Origin` |
| origem desconhecida | não emitir headers CORS | não | não reflita o valor recebido |

Se você está começando o backend, leia primeiro o guia de [API REST em Zig](/artigos/zig-api-rest-completa/) e o tutorial de [servidor HTTP com `std.http.Server`](/tutoriais/zig-http-server/). CORS deve entrar como uma política de borda pequena, não como lógica espalhada por cada handler.

## O que CORS realmente faz

CORS (*Cross-Origin Resource Sharing*) é um protocolo entre o navegador e o servidor. Ele decide se JavaScript executado em uma origem pode ler a resposta de outra origem.

Uma origem é a combinação de:

```text
esquema + host + porta
```

Portanto, estas origens são diferentes:

```text
https://app.exemplo.com.br
http://app.exemplo.com.br
https://api.exemplo.com.br
https://app.exemplo.com.br:8443
```

O navegador pode até enviar a requisição em alguns casos, mas bloqueia o acesso da página à resposta quando os headers CORS não autorizam a leitura. Isso explica um erro comum: a API parece funcionar no `curl` e falhar no frontend. O `curl` não aplica a política do navegador.

CORS também não impede alguém de chamar sua API diretamente. Um bot pode copiar os mesmos headers, e um serviço backend pode ignorar completamente a política. A proteção real continua sendo composta por autenticação, autorização por recurso, validação, limites de body, rate limiting e logs. Para essa camada operacional, veja [rate limiting com token bucket](/artigos/zig-rate-limiting-token-bucket/) e configuração segura de segredos.

## Requisição simples e preflight

O navegador não faz preflight para toda chamada. Algumas requisições consideradas “simples” podem ser enviadas diretamente, normalmente com métodos como `GET`, `HEAD` ou certos `POST`, desde que usem apenas headers e content types limitados pela especificação.

Quando o frontend tenta enviar algo como:

```http
PUT /usuarios/42 HTTP/1.1
Origin: https://app.exemplo.com.br
Authorization: Bearer ...
Content-Type: application/json
```

o navegador tende a consultar a permissão primeiro:

```http
OPTIONS /usuarios/42 HTTP/1.1
Origin: https://app.exemplo.com.br
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
```

O servidor deve responder sem executar a atualização:

```http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.exemplo.com.br
Access-Control-Allow-Methods: GET, PUT, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Vary: Origin
```

Somente depois dessa aprovação o navegador envia o `PUT` real. Se a rota `OPTIONS` cair no handler de autenticação, tentar ler JSON ou retornar `404`, o frontend verá um erro de CORS antes que a operação principal aconteça.

## Modele a política separadamente

Uma estrutura pequena evita condicionais duplicadas:

```zig
const CorsPolicy = struct {
    allowed_origins: []const []const u8,
    allowed_methods: []const u8 = "GET, POST, PUT, PATCH, DELETE, OPTIONS",
    allowed_headers: []const u8 = "Authorization, Content-Type, Idempotency-Key",
    exposed_headers: []const u8 = "ETag, X-Request-Id",
    allow_credentials: bool = true,
    max_age_seconds: u32 = 600,
};
```

A lista deve vir da configuração validada no startup, não de uma substring improvisada. Em produção, algo como `CORS_ALLOWED_ORIGINS=https://app.exemplo.com.br,https://admin.exemplo.com.br` pode ser convertido em uma lista e validado antes de o servidor aceitar tráfego.

Evite regras como:

```text
origin termina com "exemplo.com.br"
```

A origem `https://naoexemplo.com.br` também termina com esse texto. Mesmo uma checagem de sufixo com ponto precisa considerar normalização, portas e domínios internacionais. Para a maioria das APIs, igualdade exata é mais fácil de revisar.

## Validação explícita da origem em Zig

A função central pode ser independente do servidor HTTP:

```zig
const std = @import("std");

fn allowedOrigin(policy: CorsPolicy, origin: []const u8) ?[]const u8 {
    for (policy.allowed_origins) |allowed| {
        if (std.mem.eql(u8, origin, allowed)) return allowed;
    }
    return null;
}

test "aceita somente origem cadastrada" {
    const origins = [_][]const u8{
        "https://app.exemplo.com.br",
        "http://localhost:3000",
    };
    const policy = CorsPolicy{ .allowed_origins = &origins };

    try std.testing.expect(allowedOrigin(
        policy,
        "https://app.exemplo.com.br",
    ) != null);
    try std.testing.expect(allowedOrigin(
        policy,
        "https://app.exemplo.com.br.atacante.test",
    ) == null);
}
```

Retornar o valor da allowlist, em vez de refletir diretamente o header recebido, reduz a chance de uma validação frouxa virar vulnerabilidade. Antes de comparar, imponha um limite pequeno ao tamanho de `Origin`; headers gigantes devem ser rejeitados na camada HTTP geral.

## Aplicando headers à resposta

O adaptador concreto depende da versão de `std.http.Server`, mas a decisão pode produzir uma lista de headers que o handler adiciona à resposta:

```zig
const CorsHeaders = struct {
    allow_origin: []const u8,
    allow_methods: []const u8,
    allow_headers: []const u8,
    expose_headers: []const u8,
    allow_credentials: bool,
    max_age_seconds: u32,
};

fn evaluateCors(policy: CorsPolicy, origin: ?[]const u8) ?CorsHeaders {
    const received = origin orelse return null;
    const allowed = allowedOrigin(policy, received) orelse return null;

    return .{
        .allow_origin = allowed,
        .allow_methods = policy.allowed_methods,
        .allow_headers = policy.allowed_headers,
        .expose_headers = policy.exposed_headers,
        .allow_credentials = policy.allow_credentials,
        .max_age_seconds = policy.max_age_seconds,
    };
}
```

Na resposta real, traduza esse resultado para:

```text
Access-Control-Allow-Origin: <origem aprovada>
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: ETag, X-Request-Id
Vary: Origin
```

`Access-Control-Expose-Headers` é necessário quando o JavaScript precisa ler headers que não fazem parte da lista exposta automaticamente pelo navegador. É comum liberar `ETag`, `Location`, `X-Request-Id` ou um header de paginação; não exponha informação interna sem necessidade.

## Como tratar OPTIONS antes do roteamento

O fluxo recomendado é:

```text
1. validar tamanho e sintaxe básica dos headers
2. ler Origin
3. identificar OPTIONS com Access-Control-Request-Method
4. avaliar a política CORS
5. responder 204 ou rejeitar
6. para a chamada real, executar autenticação e handler
7. adicionar headers CORS à resposta final quando a origem for permitida
```

Um esqueleto conceitual:

```zig
fn handleRequest(req: *Request, policy: CorsPolicy) !void {
    const origin = req.header("origin");
    const cors = evaluateCors(policy, origin);

    if (req.method == .OPTIONS and
        req.header("access-control-request-method") != null)
    {
        const approved = cors orelse {
            return respondNoContent(req, .forbidden);
        };

        if (!requestedMethodAllowed(req, policy) or
            !requestedHeadersAllowed(req, policy))
        {
            return respondNoContent(req, .forbidden);
        }

        return respondPreflight(req, approved);
    }

    // Autenticação, autorização, roteamento e handler normal.
    const response = try route(req);
    if (cors) |headers| addCorsHeaders(response, headers);
    try response.send();
}
```

Os nomes `Request`, `header`, `respondPreflight` e `send` são deliberadamente adaptadores. O ponto importante é a ordem: o preflight não deve executar efeito colateral, e a resposta real também precisa incluir `Access-Control-Allow-Origin`. Aprovar somente `OPTIONS` não basta.

## Valide método e headers solicitados

Não devolva uma lista ampla sem olhar o pedido. O preflight informa o método desejado em `Access-Control-Request-Method` e os headers em `Access-Control-Request-Headers`.

A validação deve:

- comparar método sem aceitar valores inventados;
- normalizar nomes de headers de forma case-insensitive;
- dividir a lista por vírgula;
- remover espaços externos;
- rejeitar header fora da allowlist;
- impor limite de quantidade e tamanho;
- não confundir header vazio com autorização irrestrita.

Se a API só aceita `GET` e `POST`, não anuncie `DELETE`. Se não usa `X-Admin-Override`, não o inclua em `Access-Control-Allow-Headers`. A resposta CORS também funciona como documentação de superfície da API.

## Credenciais, cookies e CSRF

`Access-Control-Allow-Credentials: true` permite que o navegador disponibilize ao frontend uma resposta associada a credenciais, desde que a chamada do JavaScript também opte por enviá-las. Isso costuma aparecer com cookies de sessão:

```javascript
fetch("https://api.exemplo.com.br/perfil", {
  credentials: "include"
});
```

Nesse modo:

- `Access-Control-Allow-Origin` não pode ser `*`;
- a origem deve ser exata;
- o cookie deve ter atributos adequados, como `Secure`, `HttpOnly` e política `SameSite` coerente;
- operações mutáveis ainda precisam de defesa contra CSRF quando a autenticação é automática por cookie;
- `Origin` e, quando apropriado, `Referer` podem participar da defesa, mas não substituem token CSRF em todos os desenhos.

Bearer tokens enviados explicitamente em `Authorization` não tornam a API automaticamente segura. Um XSS no frontend pode acessar o token, e uma política CORS permissiva pode expor respostas a origens indevidas.

## Por que `Vary: Origin` é obrigatório no caso dinâmico

Quando o servidor devolve uma origem diferente conforme a requisição, caches precisam saber que a resposta varia por `Origin`:

```http
Vary: Origin
```

Sem isso, um CDN ou proxy pode armazenar uma resposta liberada para `https://app.exemplo.com.br` e entregá-la a outra origem. Se a resposta já possui `Vary: Accept-Encoding`, não substitua o valor; combine os campos corretamente.

O mesmo cuidado vale para cache de preflight. `Access-Control-Max-Age: 600` reduz chamadas `OPTIONS`, mas também prolonga uma política antiga no navegador. Comece com alguns minutos, valide o comportamento e evite valores enormes durante migrações de domínio.

## CORS no Nginx ou no serviço Zig?

As duas opções funcionam, mas escolha um único dono da política.

**No serviço Zig**, a política pode variar por rota e fica perto da autenticação. É mais fácil testar como código e evitar liberar métodos inexistentes.

**No Nginx, Caddy ou gateway**, a organização consegue padronizar vários serviços. Porém, configurações com condicionais e headers duplicados ficam difíceis de revisar. Um proxy que adiciona `Allow-Origin: *` por cima de uma origem específica pode gerar resposta inválida.

Para uma API pequena, manter a decisão no Zig e deixar o proxy apenas repassar `Origin` e `OPTIONS` costuma ser previsível. Em plataformas maiores, um gateway central pode ser adequado desde que haja testes de contrato. O guia de [Zig por trás de Nginx](/artigos/zig-nginx-proxy-reverso-load-balancing/) cobre a fronteira entre proxy e aplicação.

## Como testar com curl

Teste uma requisição real autorizada:

```bash
curl -i https://api.exemplo.com.br/perfil \
  -H 'Origin: https://app.exemplo.com.br'
```

Teste o preflight:

```bash
curl -i -X OPTIONS https://api.exemplo.com.br/usuarios/42 \
  -H 'Origin: https://app.exemplo.com.br' \
  -H 'Access-Control-Request-Method: PUT' \
  -H 'Access-Control-Request-Headers: authorization, content-type'
```

Teste uma origem atacante:

```bash
curl -i -X OPTIONS https://api.exemplo.com.br/usuarios/42 \
  -H 'Origin: https://app.exemplo.com.br.atacante.test' \
  -H 'Access-Control-Request-Method: PUT'
```

A última resposta não deve refletir a origem. No teste automatizado, cubra ainda:

1. origem permitida em `GET`;
2. origem ausente, como chamada server-to-server;
3. origem desconhecida;
4. preflight com método permitido;
5. preflight com método proibido;
6. header permitido com capitalização diferente;
7. header desconhecido;
8. credenciais sem wildcard;
9. presença de `Vary: Origin`;
10. resposta de erro `401`, `403`, `404` e `500` com CORS coerente.

Esse último item é importante: se apenas respostas `200` recebem CORS, o navegador esconde o body do erro e o frontend mostra “falha de CORS” no lugar da mensagem útil da API.

## Erros comuns de configuração

### Refletir qualquer Origin

Isto é praticamente equivalente a liberar todo mundo:

```text
Access-Control-Allow-Origin: <valor recebido>
```

Só reflita após igualdade com uma allowlist confiável.

### Usar wildcard com cookies

A combinação abaixo é inválida para credenciais:

```http
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
```

Escolha uma origem específica ou remova credenciais.

### Liberar somente no ambiente local

`localhost:3000`, `localhost:5173` e `127.0.0.1:5173` são origens diferentes. Cadastre apenas as usadas pela equipe e mantenha a configuração de desenvolvimento separada da produção.

### Exigir autenticação no preflight

O navegador não envia necessariamente as credenciais da operação final no `OPTIONS`. Avalie origem, método e headers no preflight; autentique a requisição real.

### Tratar CORS como controle de acesso

Uma origem aprovada não prova quem é o usuário. O handler ainda precisa verificar se aquele usuário pode ler ou alterar o recurso solicitado.

## Observabilidade sem vazar dados

Registre decisões CORS com baixa cardinalidade:

```text
cors_preflight result=allowed route=/usuarios/:id method=PUT
cors_preflight result=denied reason=origin_not_allowed
cors_preflight result=denied reason=header_not_allowed
```

Evite colocar a origem completa como label livre de Prometheus, pois hosts aleatórios geram cardinalidade sem limite. Uma métrica útil pode separar apenas `allowed`, `denied_origin`, `denied_method` e `denied_header`. Para investigar, registre a origem normalizada em log estruturado com retenção e controles apropriados.

## Checklist de produção

- [ ] Origens de produção usam igualdade exata com allowlist.
- [ ] Configuração de localhost não é carregada em produção.
- [ ] Preflight `OPTIONS` é tratado antes do handler e sem efeito colateral.
- [ ] Métodos anunciados correspondem às rotas reais.
- [ ] Headers solicitados são comparados de forma case-insensitive.
- [ ] `Access-Control-Allow-Origin` aparece também em respostas de erro aplicáveis.
- [ ] Credenciais nunca são combinadas com origem `*`.
- [ ] Cookies têm `Secure`, `HttpOnly` e `SameSite` revisados.
- [ ] Operações autenticadas por cookie têm defesa contra CSRF.
- [ ] Respostas dinâmicas incluem `Vary: Origin`.
- [ ] `Access-Control-Max-Age` tem duração moderada e revisável.
- [ ] Proxy e aplicação não adicionam headers CORS conflitantes.
- [ ] Testes cobrem origem atacante parecida com o domínio legítimo.
- [ ] CORS não é tratado como substituto de autenticação ou autorização.

## Perguntas frequentes

### Como liberar CORS em uma API Zig?

Valide `Origin` contra uma lista explícita, devolva `Access-Control-Allow-Origin` apenas para origens autorizadas e responda ao preflight `OPTIONS` com os métodos e headers realmente aceitos. Não basta adicionar um asterisco a todas as respostas.

### Posso usar Access-Control-Allow-Origin com asterisco e credenciais?

Não. Quando cookies ou credenciais do navegador estão habilitados, `Access-Control-Allow-Origin` precisa conter uma origem específica. O valor `*` não pode ser combinado com `Access-Control-Allow-Credentials: true`.

### Por que o navegador envia OPTIONS antes do POST ou PUT?

Essa requisição é o preflight. O navegador verifica se a origem, o método e os headers pretendidos são aceitos antes de enviar uma operação que não se enquadra como requisição simples.

### CORS protege a API contra curl, bots ou chamadas entre servidores?

Não. CORS é aplicado pelo navegador. `curl`, scripts e backends continuam capazes de chamar o endpoint, portanto autenticação, autorização, rate limiting e validação permanecem obrigatórios.

### Como testar CORS sem depender de um frontend?

Use `curl` enviando `Origin` e, no preflight, `Access-Control-Request-Method` e `Access-Control-Request-Headers`. Teste origens permitidas e rejeitadas, verifique `Vary: Origin` e confirme que respostas de erro também seguem a política.

## Conclusão

Uma implementação segura de CORS em Zig não precisa virar um framework. Ela pode ser composta por três partes pequenas: uma allowlist validada no startup, uma função pura que decide se a origem é aceita e um adaptador HTTP que trata preflight e adiciona headers à resposta final.

O principal é manter o contrato explícito. Origem não é identidade, preflight não executa a operação, wildcard não combina com credenciais e CORS não bloqueia clientes fora do navegador. Com essas fronteiras claras, o frontend consegue conversar com a API sem transformar uma conveniência de desenvolvimento em uma liberação acidental de produção.

Para completar a camada HTTP, conecte esta política ao guia de [autenticação JWT em Zig](/artigos/zig-jwt-autenticacao-api/), aos [limites e health checks do servidor em produção](/artigos/zig-http-server-producao/) e ao checklist de [TLS e certificados](/artigos/zig-tls-https-certificados-mtls/).
