Código de status HTTP · Redirecionamento (3xx)
304 Not Modified
O status 304 Not Modified significa que a cópia que o navegador ou o cache já tem continua atual, então o servidor não mandou o corpo de novo. É a resposta normal a uma requisição condicional com If-None-Match ou If-Modified-Since, não um erro.
| Classe | 3xx, Redirecionamento |
|---|---|
| Definido em | RFC 9110 §15.4.5 |
| Pode ir para o cache por padrão | Não é guardado; atualiza a cópia que já está em cache |
| Pode repetir o pedido | Não precisa; o cliente reaproveita a cópia que já tem guardada |
| Cabeçalhos relevantes |
|
O que significa o 304
Um cliente que guardou uma resposta pode revalidá-la mandando de volta o ETag que recebeu, no If-None-Match, ou a data do Last-Modified, no If-Modified-Since. Se nada mudou, o servidor responde 304 e o cliente usa o que já tem. A RFC 9110, seção 15.4.5, exige que o 304 traga os cabeçalhos Cache-Control, Content-Location, Date, ETag, Expires e Vary que a resposta 200 teria, porque o cache usa esses valores para atualizar a cópia guardada.
O 304 termina nos cabeçalhos: ele nunca pode ter corpo. A economia é de banda, não de tempo, porque o cliente ainda espera uma ida e volta inteira pela resposta. Para nem fazer o pedido é preciso validade (Cache-Control: max-age), e aí o DevTools mostra 200 com "(memory cache)" ou "(disk cache)" em vez de 304.
Para buscadores o 304 é um atalho, não um sinal: os rastreadores do Google entendem que o conteúdo é o mesmo do último rastreamento, e isso não muda nada na indexação.
Quando usar
- Automaticamente, para arquivos estáticos: nginx, Apache, express.static e o http.FileServer do Go comparam os validadores e respondem 304 sem nenhum código seu.
- Em respostas de API que mudam pouco: envie um ETag (hash do conteúdo ou número de versão) com Cache-Control: no-cache, assim o cliente confere sempre mas só baixa quando algo mudou.
- Só para GET e HEAD. Quando If-None-Match ou If-Match falham em PUT, PATCH ou DELETE, a resposta é 412 Precondition Failed.
Causas comuns
Se você está visitando o site
- Ver 304 na aba Rede é esperado: o navegador revalidou um arquivo do cache e o servidor confirmou que ele está atual.
- Uma página ou CSS que não atualiza costuma ser culpa de um max-age longo, não do 304; nesse caso o navegador nem perguntou ao servidor.
Se você administra o servidor
- ETags diferentes entre servidores atrás de um balanceador (versões antigas do Apache usavam o inode do arquivo), então a revalidação nunca bate e todo pedido volta como 200 completo.
- Compressão ou proxies mexendo nos validadores: o nginx transforma um ETag forte em fraco quando comprime com gzip, e um proxy que remove ETag e Last-Modified acaba com o 304.
- Código de front-end que define If-None-Match por conta própria no fetch() recebe o 304 cru, sem corpo, e quebra ao interpretar o JSON; se o cache do navegador cuidasse da revalidação, viria um 200 do cache.
- Validadores que não mudam quando o conteúdo muda, como um ETag feito de um número de versão que ninguém incrementou, ou um Last-Modified vindo de datas de arquivo preservadas ou fixadas pelo deploy.
Como resolver
Se você está visitando o site
- Não há o que corrigir. Se a página parecer desatualizada, faça um recarregamento forçado (Ctrl+Shift+R, ou Cmd+Shift+R no macOS), que baixa tudo de novo em vez de revalidar.
Se você administra o servidor
- Gere ETags a partir do conteúdo (um hash) ou de uma versão que muda a cada deploy, nunca de inode ou de dados específicos do servidor.
- Teste a revalidação de ponta a ponta: curl -sI URL para ler o ETag, depois curl -sI -H 'If-None-Match: "<etag>"' URL e espere um 304.
- No código do cliente, deixe o cache do navegador cuidar dos validadores, ou trate o status 304 explicitamente e reaproveite sua própria cópia.
- Faça o 304 trazer os mesmos Cache-Control, ETag e Vary da 200, senão os caches ficam com dados de validade velhos.
Como enviar um 304
// res.send() and express.static already answer 304 on a matching
// ETag. Doing it yourself with a version-based ETag:
app.get('/api/catalog', async (req, res) => {
const catalog = await getCatalog();
res.set('ETag', `"${catalog.version}"`);
res.set('Cache-Control', 'no-cache');
if (req.fresh) return res.status(304).end();
res.send(catalog);
});// app/api/catalog/route.ts
export async function GET(request: Request) {
const catalog = await getCatalog();
const etag = `"${catalog.version}"`;
const headers = { ETag: etag, 'Cache-Control': 'no-cache' };
const sent = request.headers.get('if-none-match') ?? '';
if (sent.split(/\s*,\s*/).includes(etag)) {
return new Response(null, { status: 304, headers });
}
return new Response(JSON.stringify(catalog), {
headers: { ...headers, 'Content-Type': 'application/json' }
});
}// http.ServeContent and http.FileServer handle If-None-Match and
// If-Modified-Since for you. The manual version (exact match only):
mux.HandleFunc("GET /api/catalog", func(w http.ResponseWriter, r *http.Request) {
etag := `"` + catalog.Version + `"`
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "no-cache")
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified) // 304, no body
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(catalog)
})from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/api/catalog")
def get_catalog(request: Request):
catalog = load_catalog()
etag = f'"{catalog["version"]}"'
headers = {"ETag": etag, "Cache-Control": "no-cache"}
if request.headers.get("if-none-match") == etag:
return Response(status_code=304, headers=headers)
return JSONResponse(catalog, headers=headers)# nginx answers 304 for static files on its own; both
# directives below are already the defaults.
location /assets/ {
etag on;
if_modified_since exact;
add_header Cache-Control "no-cache";
}Costuma ser confundido com
- 304 vs 200
- Um "200 (from disk cache)" no DevTools significa que nenhum pedido saiu; o 304 significa que o navegador perguntou e o servidor confirmou a cópia.
- 304 vs 412
- O 412 é a resposta de pré-condição falha para métodos que não são GET nem HEAD, como um PUT cujo If-Match não bate mais.
Perguntas frequentes
- O 304 Not Modified é um erro?
- Não. É uma revalidação bem-sucedida: o servidor avisa o cliente que a cópia que ele tem continua correta. O monitoramento deve contá-lo como sucesso, e em geral ele indica que o cache está funcionando.
- Qual a diferença entre 304 e 200 (from disk cache)?
- No 200 (from disk cache) o navegador usou a cópia sem falar com o servidor, porque ela ainda estava válida. No 304 a cópia tinha expirado ou exigia revalidação, então o navegador perguntou e o servidor respondeu que nada mudou.
- Por que recebo 304 se o arquivo mudou?
- O validador não mudou junto com o conteúdo. As causas comuns são um ETag derivado de um número de versão que não foi incrementado, ou um Last-Modified vindo de datas de arquivo que o deploy preservou ou fixou. Faça os validadores dependerem do próprio conteúdo.
- A resposta 304 tem corpo?
- Não. A RFC 9110 diz que o 304 termina no fim dos cabeçalhos e não pode ter conteúdo nem trailers. O cliente usa o corpo da resposta que já tinha guardado.
Revisado em por Arielton Oberek.