---
title: "Cache HTTP em Zig com ETag, If-None-Match e 304 Not Modified"
url: "https://ziglang.com.br/artigos/zig-http-cache-etag-if-none-match/"
markdown_url: "https://ziglang.com.br/artigos/zig-http-cache-etag-if-none-match.MD"
description: "Como implementar cache HTTP condicional em Zig com ETag, If-None-Match, 304 Not Modified, arquivos atômicos, limites, validação e testes locais."
date: "2026-08-10"
author: ""
---

# Cache HTTP em Zig com ETag, If-None-Match e 304 Not Modified

Como implementar cache HTTP condicional em Zig com ETag, If-None-Match, 304 Not Modified, arquivos atômicos, limites, validação e testes locais.


Para implementar **cache HTTP em Zig**, guarde o corpo da resposta junto com o valor de `ETag`. Na próxima requisição, envie esse valor no header `If-None-Match`. Se o servidor responder `304 Not Modified`, reutilize o corpo local; se responder `200 OK`, valide a nova resposta e substitua corpo e metadados de forma atômica.

Esse padrão reduz tráfego, latência e processamento sem inventar uma regra própria de expiração. Ele funciona bem em CLIs, agentes locais, atualizadores, catálogos, manifests de release e integrações que consultam recursos alterados com pouca frequência. A regra mais importante é simples: **um `304` só pode ser usado quando existe uma entrada de cache completa e compatível com a requisição atual**.

Como `std.http.Client` ainda pode mudar antes do Zig 1.0, confira `zig version` e as definições instaladas de `Client.fetch`, `Request` e headers. Os nomes exatos da API podem variar; o protocolo — enviar `If-None-Match`, distinguir `200` de `304` e publicar arquivos com segurança — permanece o mesmo.

## Resposta rápida

| Resposta do servidor | Ação do cliente Zig |
|---|---|
| `200 OK` com `ETag` | validar e salvar o novo corpo com o novo ETag |
| `200 OK` sem `ETag` | usar a resposta, mas não assumir revalidação futura por ETag |
| `304 Not Modified` | carregar o corpo local correspondente |
| `404 Not Found` | não devolver silenciosamente um cache antigo como se fosse atual |
| `429 Too Many Requests` | respeitar a política de retry e `Retry-After`, quando presente |
| `500` a `504` | decidir explicitamente se cache vencido pode ser usado |
| cache ausente ou corrompido | fazer uma requisição normal, sem `If-None-Match` |

Se você ainda não fez requisições em Zig, comece pelo tutorial de [`std.http.Client`, GET, POST e headers](/tutoriais/zig-http-client/). Para salvar artefatos completos com checksum, veja [download de arquivos com SHA-256 e escrita atômica](/artigos/zig-download-arquivos-http-checksum/).

## Como ETag e If-None-Match funcionam

`ETag` é um identificador fornecido pelo servidor para uma representação de um recurso. Ele pode refletir um hash do conteúdo, uma versão interna, um timestamp serializado ou qualquer mecanismo escolhido pelo serviço. O cliente não deve interpretar esse valor; deve armazená-lo e devolvê-lo exatamente como recebeu, inclusive aspas e o prefixo `W/` de um ETag fraco.

Primeira requisição:

```http
GET /catalogo.json HTTP/1.1
Host: api.exemplo.com
Accept: application/json
```

Resposta:

```http
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "catalogo-v42"
Content-Length: 18420

{"versao":42,"itens":[...]}
```

O cliente salva o corpo e o ETag. Na consulta seguinte:

```http
GET /catalogo.json HTTP/1.1
Host: api.exemplo.com
Accept: application/json
If-None-Match: "catalogo-v42"
```

Se a representação não mudou:

```http
HTTP/1.1 304 Not Modified
ETag: "catalogo-v42"
```

A resposta `304` não traz um novo corpo para substituir o anterior. Ela informa que a representação local associada ao validador continua utilizável. Se o conteúdo mudou, o servidor responde `200` com o novo corpo e, normalmente, um novo ETag.

## ETag forte e ETag fraco

Um ETag forte costuma aparecer entre aspas:

```text
"catalogo-v42"
```

Um ETag fraco usa o prefixo `W/`:

```text
W/"catalogo-v42"
```

O validador fraco indica equivalência sem exigir identidade byte a byte. Duas respostas podem ser semanticamente equivalentes mesmo com pequenas diferenças de serialização. Para cache de uma API, isso pode ser suficiente. Para confirmar a integridade exata de um binário, não substitua SHA-256 por ETag: use o fluxo de checksum do artefato.

Não remova aspas, não converta para minúsculas e não tente recalcular o ETag. Trate o header como uma sequência opaca definida pelo servidor.

## Estrutura mínima do cache local

Uma entrada precisa relacionar, no mínimo:

- URL ou chave canônica da requisição;
- ETag recebido;
- caminho do corpo salvo;
- data da última validação;
- versão do formato local;
- atributos que alteram a representação, como `Accept` ou idioma.

Para uma CLI pequena, dois arquivos são suficientes:

```text
.cache/catalogo.body
.cache/catalogo.meta
```

O arquivo de metadados pode usar um formato simples:

```text
version=1
url=https://api.exemplo.com/catalogo.json
accept=application/json
etag="catalogo-v42"
```

Em uma ferramenta maior, SQLite pode facilitar locking, índices e migrações. O artigo de Zig com SQLite para ferramentas locais mostra quando o banco embutido vale mais do que arquivos soltos.

A chave de cache não deve depender apenas do caminho da URL. Query string, método, header `Accept`, autenticação e outros campos podem alterar a representação. Nunca compartilhe uma resposta privada entre usuários porque duas URLs parecem iguais.

## Modelo do fluxo em Zig

Separe protocolo HTTP, armazenamento e política. O núcleo pode retornar apenas três resultados:

```zig
const FetchResult = union(enum) {
    fresh: []u8,
    cached: []u8,
};

const CacheMetadata = struct {
    etag: ?[]const u8,
    url: []const u8,
    accept: []const u8,
};
```

A função de alto nível segue esta ordem:

```text
carregar metadados e verificar corpo local
montar headers, incluindo If-None-Match quando possível
executar GET
se 200: validar corpo -> salvar temporários -> publicar -> retornar fresh
se 304: carregar corpo local -> retornar cached
qualquer outro status: aplicar política explícita de erro
```

Um esqueleto deliberadamente isolado da assinatura exata de `std.http` fica assim:

```zig
fn getWithCache(
    allocator: std.mem.Allocator,
    client: *std.http.Client,
    url: []const u8,
    cache: *CacheStore,
) !FetchResult {
    const entry = try cache.loadMetadata(allocator, url);
    defer if (entry) |metadata| metadata.deinit(allocator);

    const response = try requestResource(
        allocator,
        client,
        url,
        if (entry) |metadata| metadata.etag else null,
    );
    defer response.deinit(allocator);

    switch (response.status) {
        .ok => {
            try validateResponse(response);
            try cache.replaceAtomically(.{
                .url = url,
                .etag = response.etag,
                .body = response.body,
            });
            return .{ .fresh = try allocator.dupe(u8, response.body) };
        },
        .not_modified => {
            const body = try cache.loadBody(allocator, url);
            return .{ .cached = body };
        },
        else => return error.StatusHttpInesperado,
    }
}
```

`requestResource` é a única função que precisa acompanhar mudanças da stdlib. Ela monta `extra_headers` ou uma `Request`, preserva o `ETag` recebido e aplica limite ao body. `CacheStore` não precisa saber nada sobre sockets ou HTTP.

## Montando o header condicional

O header deve ser enviado somente quando o cache está completo. Conceitualmente:

```zig
const conditional_headers = if (etag) |value|
    &[_]std.http.Header{
        .{ .name = "Accept", .value = "application/json" },
        .{ .name = "If-None-Match", .value = value },
    }
else
    &[_]std.http.Header{
        .{ .name = "Accept", .value = "application/json" },
    };
```

Na versão de Zig instalada no seu projeto, o tipo do header ou a forma de passá-lo pode ter outro nome. Evite espalhar essa construção pela aplicação. Uma função pequena como `requestResource` reduz o custo de migração entre releases.

Não envie um ETag encontrado em metadados quando o arquivo de body está ausente, vazio por corrupção ou pertence a outra chave. Caso contrário, o servidor pode devolver `304` e o cliente não terá representação para usar.

## Salvando corpo e metadados sem criar pares inconsistentes

O cache possui dois elementos que precisam concordar. Se o processo salvar primeiro o novo ETag e cair antes de salvar o body, a próxima requisição pode receber `304` para uma representação que não existe localmente.

Use temporários:

```text
catalogo.body.tmp
catalogo.meta.tmp
```

Fluxo recomendado:

1. validar status, tamanho e formato do novo corpo;
2. escrever `catalogo.body.tmp`;
3. sincronizar quando durabilidade for necessária;
4. escrever `catalogo.meta.tmp` com o ETag correspondente;
5. publicar o body;
6. publicar os metadados por último;
7. remover temporários abandonados na próxima inicialização.

Publicar os metadados por último faz deles o ponteiro para uma entrada completa. Para concorrência entre vários processos, adicione lock por chave ou use SQLite com transação. Apenas `rename` não resolve dois escritores disputando a mesma entrada.

A técnica de temporário e rename também aparece no guia de File I/O em Zig e no downloader com checksum citado anteriormente.

## Limites continuam obrigatórios

Cache não elimina a necessidade de limite de memória. Uma resposta `200` pode ser muito maior que a versão anterior, ter `Content-Length` ausente ou vir de um servidor com comportamento incorreto.

Aplique um teto durante a leitura:

```text
ler bloco
somar bytes
se total > limite: abortar
escrever no temporário ou buffer controlado
```

Para JSON pequeno, um buffer limitado pode ser adequado. Para catálogos e artefatos grandes, faça streaming para o arquivo temporário. Só publique depois de validar o conteúdo mínimo esperado. Um `200` com HTML de erro entregue por um proxy não deve substituir silenciosamente um JSON válido no cache.

Validações úteis incluem:

- status exatamente permitido;
- `Content-Type` compatível;
- tamanho máximo;
- JSON parseável, quando aplicável;
- schema ou campos obrigatórios;
- checksum, para artefatos que possuem hash oficial.

## Cache privado, Authorization e Vary

Requisições autenticadas exigem cuidado. Se a resposta depende de `Authorization`, cookie, tenant ou usuário, a chave de cache precisa representar esse contexto — ou o cache deve ser desabilitado. Não coloque o token em texto puro no nome do arquivo. Use uma identidade interna ou um hash apropriado da chave de isolamento, sem registrar o segredo em logs.

O header `Vary` informa quais headers influenciam a representação, como:

```http
Vary: Accept-Encoding, Accept-Language
```

Um cliente de propósito específico pode definir sua chave antecipadamente porque controla os headers enviados. Um cache HTTP genérico precisa respeitar `Vary` de maneira completa. Se isso estiver fora do escopo, documente que a ferramenta aceita apenas uma combinação fixa de `Accept`, idioma e codificação.

Também não confunda ETag com autorização. Possuir um validador não dá direito de acessar o recurso. Continue enviando a credencial exigida e trate `401` e `403` como erros de autenticação ou permissão.

## Quando usar Last-Modified

Alguns servidores não fornecem `ETag`, mas retornam `Last-Modified`. Nesse caso, o cliente pode salvar o valor e enviar `If-Modified-Since` na próxima requisição.

ETag costuma ser mais preciso porque não depende da resolução de timestamps. Se o servidor oferece ambos, prefira `If-None-Match` para revalidação. Não invente um `Last-Modified` local com base no horário do download: ele deve vir do servidor.

A arquitetura pode suportar os dois validadores:

```zig
const Validator = union(enum) {
    etag: []const u8,
    last_modified: []const u8,
};
```

Mas comece com um mecanismo bem testado antes de ampliar a matriz de estados.

## Cache vencido em caso de erro

Usar uma resposta antiga quando a origem falha é uma decisão de produto, não um comportamento automático. Para um catálogo informativo, “stale if error” por alguns minutos pode ser aceitável. Para permissões, revogações, preço, configuração de segurança ou comando operacional, dados antigos podem ser perigosos.

Modele a política explicitamente:

```zig
const StalePolicy = struct {
    allow_on_network_error: bool,
    max_stale_seconds: u64,
};
```

Registre quando o resultado é antigo e exponha essa informação ao chamador. Não retorne `cached` como se fosse `fresh`. O tipo `FetchResult` ajuda a impedir que a diferença desapareça.

Retries também devem ser limitados. Para `429` e erros transitórios, use backoff com jitter; para `404`, `401` e `403`, repetir imediatamente raramente ajuda. Veja o guia de [timeout, retry e circuit breaker em Zig](/artigos/zig-circuit-breaker-timeout-retry/) para separar falhas transitórias de permanentes.

## Como testar localmente

Um teste útil precisa controlar o servidor. O cenário básico:

1. primeira chamada sem `If-None-Match`;
2. servidor responde `200`, body `versao 1` e ETag `"v1"`;
3. segunda chamada envia `If-None-Match: "v1"`;
4. servidor responde `304`;
5. cliente devolve exatamente o body salvo;
6. servidor muda para `versao 2` e ETag `"v2"`;
7. terceira chamada recebe `200` e substitui a entrada.

Cubra também:

- `304` com body local ausente;
- metadados truncados;
- `200` acima do limite;
- `200` com JSON inválido;
- ETag fraco preservado sem alteração;
- URL igual com `Accept` diferente;
- falha entre publicação do body e dos metadados;
- dois processos tentando atualizar a mesma chave;
- `404`, `401`, `429` e `503`;
- cache antigo permitido e proibido pela política.

Inspecione os headers recebidos pelo servidor fake. Um teste que valida apenas o body final pode passar mesmo quando o cliente nunca enviou `If-None-Match`.

## Observabilidade do cache

Uma cache invisível é difícil de depurar. Registre métricas ou contadores para:

- `cache_hit_304`;
- `cache_miss`;
- `cache_refresh_200`;
- `cache_corrupt`;
- `cache_stale_used`;
- bytes evitados;
- latência da revalidação;
- erros por status HTTP.

Nos logs, inclua uma chave segura do recurso, status e decisão tomada. Não registre `Authorization`, cookies, query strings sensíveis ou o conteúdo completo do ETag se ele puder carregar informação interna. O artigo de [observabilidade em Zig com logs e Prometheus](/artigos/zig-observabilidade-logs-prometheus/) ajuda a transformar esses eventos em sinais operacionais.

## Checklist de produção

- [ ] O ETag é armazenado exatamente como recebido.
- [ ] `If-None-Match` só é enviado quando o body correspondente existe.
- [ ] `200` e `304` seguem caminhos separados e testados.
- [ ] O body possui limite aplicado durante a leitura.
- [ ] O conteúdo novo é validado antes de substituir a entrada.
- [ ] Body e metadados são publicados com temporários e ordem segura.
- [ ] A chave inclui URL, query e dimensões relevantes da representação.
- [ ] Respostas autenticadas são isoladas por usuário ou não são cacheadas.
- [ ] Existe política explícita para cache antigo em falha da origem.
- [ ] Logs não expõem credenciais.
- [ ] Concorrência entre processos usa lock ou transação.
- [ ] Testes verificam o header `If-None-Match` e a recuperação de corrupção.

## Conclusão

Cache HTTP condicional em Zig não exige um framework grande. Guarde o corpo e o ETag, envie `If-None-Match`, reutilize o cache somente diante de `304` válido e substitua a entrada apenas depois que uma resposta `200` passar por limites e validações.

O ganho vem menos da quantidade de código e mais da precisão do contrato: qual requisição gerou a entrada, quando uma representação antiga pode ser usada, como body e metadados permanecem consistentes e quais erros autorizam fallback. Essa abordagem combina com Zig porque deixa memória, I/O e estados de falha visíveis.

Para continuar, leia a referência de [`std.http.Client`]({{< relref "stdlib/std-http-client.md" >}}), o guia de [configuração de CLI com XDG e variáveis de ambiente](/artigos/zig-cli-config-xdg-env/) e o artigo de [testes completos em Zig](/artigos/zig-testes-guia-completo/) para transformar o exemplo em uma biblioteca pequena e confiável para sua ferramenta.
