Skip to content

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.

Dados sobre este código de status
Classe3xx, Redirecionamento
Definido emRFC 9110 §15.4.5
Pode ir para o cache por padrãoNão é guardado; atualiza a cópia que já está em cache
Pode repetir o pedidoNão precisa; o cliente reaproveita a cópia que já tem guardada
Cabeçalhos relevantes
  • ETag: obrigatório se a resposta 200 o teria; o cliente compara na próxima revalidação
  • Cache-Control: obrigatório se a 200 o teria; renova a validade da cópia guardada
  • Vary: deve ser igual ao da 200, para a variante certa ser atualizada no cache
  • If-None-Match: cabeçalho da requisição com o ETag guardado; se bater, sai o 304
  • If-Modified-Since: cabeçalho da requisição com a data do Last-Modified guardado; ignorado quando há If-None-Match

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

Express (Node.js)
// 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);
});
Next.js App Router (route handler)
// 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' }
  });
}
Go net/http
// 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)
})
Python FastAPI
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
# 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.