Failed to load resource: 404 (Not Found) no console
A imagem não aparece e o CSS não carrega: o caminho do arquivo está errado, e no servidor Linux até a maiúscula do nome do arquivo conta.
Failed to load resource: the server responded with a status of 404 (Not Found)
quer dizer uma coisa só: o navegador pediu um arquivo ao servidor e o servidor
respondeu que não tem nada naquele endereço. Não é erro de JavaScript, não é
erro de CSS. É endereço errado — e o arquivo, quase sempre, existe.
Todos os exemplos aqui são o site da Clínica Pata Amiga: a home, três páginas internas, uma folha de estilo e três imagens. Rodei tudo num MacBook, com Node 24.16.0 servindo os arquivos e o Chrome 151.0.7922.170 abrindo a página. Quando o assunto for deploy, entra também um container Debian 12 com Node 24.19.0.
O código técnico é 404 Not Found. Em palavras simples, o pedido chegou ao servidor, mas o caminho pedido não correspondeu a um arquivo ou rota disponível. A investigação começa pela URL exata, não pelo tipo do arquivo.
O entregador chegou ao número que não existe
Imagine uma entrega destinada à Rua das Flores, 410. O pacote pode estar no depósito e o prédio pode existir no número 401; ainda assim, aquele endereço exato não recebe a entrega. Com um recurso web acontece o mesmo: nome correto em pasta errada continua sendo uma URL errada. O servidor responde para a URL que o navegador pediu, não para a intenção de quem escreveu o HTML.
Abra a aba Network antes do primeiro conserto e copie três dados: URL completa, status e página que iniciou o pedido. Em seguida cole a URL numa nova aba. Se o 404 se repetir, você confirmou o endereço ausente; se mudar, há contexto, cache ou base URL para investigar.
Este é o index.html da clínica, com três referências:
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<title>Clínica Pata Amiga</title>
<link rel="stylesheet" href="estilo.css" />
</head>
<body>
<img src="imagens/logo.png" alt="Logo da Clínica Pata Amiga" width="120" />
<h1>Clínica Pata Amiga</h1>
<img src="img/fachada.jpeg" alt="Fachada da clínica" width="320" />
</body>
</html>E esta é a pasta de verdade:
site/css/estilo.css
site/img/banner.png.jpg
site/img/fachada.jpg
site/img/logo.png
site/index.html
site/paginas/contato.html
site/paginas/equipe.html
site/paginas/servicos.htmlOs três arquivos existem. Nenhum dos três está onde o HTML mandou procurar: a
folha de estilo está dentro de css/, a logo está em img/ e não em
imagens/, e a fachada é .jpg, não .jpeg.
A linha do console é sempre igual, e o que importa nela não é a frase — é o endereço que aparece do lado direito, o único pedaço que muda de erro para erro. No Chrome, clicar nesse endereço abre a aba Network já na requisição que falhou.
Reproduzindo com um servidor que anota cada pedido
O console mostra o que o navegador pediu. Para ver o outro lado da conversa, subi um servidor estático de vinte linhas que imprime o status de cada requisição:
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { extname, join, normalize } from 'node:path';
const PASTA = process.argv[2] ?? 'site';
const RAIZ = new URL(`./${PASTA}/`, import.meta.url).pathname;
const TIPOS = { '.html': 'text/html', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg' };
createServer(async (req, res) => {
const caminho = req.url === '/' ? '/index.html' : decodeURI(req.url);
const arquivo = join(RAIZ, normalize(caminho));
try {
const conteudo = await readFile(arquivo);
res.writeHead(200, { 'content-type': TIPOS[extname(arquivo)] ?? 'application/octet-stream' });
res.end(conteudo);
console.log(`200 ${req.url}`);
} catch {
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
res.end('404 Not Found');
console.log(`404 ${req.url}`);
}
}).listen(5175, () => console.log(`servindo ./${PASTA} em http://localhost:5175`));Do outro lado, para colar o console aqui sem depender de screenshot, abri a
página com o próprio Chrome dirigido pelo puppeteer-core. Cada mensagem do
console vira uma linha no terminal — o [error] na frente é do meu script, no
DevTools você vê só a frase:
import puppeteer from 'puppeteer-core';
const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const navegador = await puppeteer.launch({ executablePath: CHROME, headless: true });
const pagina = await navegador.newPage();
pagina.on('console', (msg) => {
const local = msg.location();
console.log(`[${msg.type()}] ${msg.text()}${local.url ? `\n ${local.url}` : ''}`);
});
await pagina.goto(process.argv[2], { waitUntil: 'networkidle0' });
await navegador.close();node servidor.js site # um terminal
node abrir.js http://localhost:5175/ # o outroRepare em duas coisas. Primeira: cada linha do console tem uma linha gêmea no
servidor, na mesma ordem. Segunda: apareceu um quarto 404 que você não escreveu
em lugar nenhum, /favicon.ico. Esse é o navegador procurando o ícone da aba
por conta própria. Se o seu site não tem ícone, ele vai estar sempre lá, e não é
o seu problema.
Caminho relativo: a conta que o navegador faz
Um caminho sem barra na frente é relativo à página aberta, não à raiz do site. O navegador pega a URL atual, joga fora o nome do arquivo e cola o caminho no lugar.
A página de serviços mora em paginas/, e copiou o caminho do index.html sem
pensar:
<link rel="stylesheet" href="css/estilo.css" />
<img src="img/logo.png" alt="Logo da Clínica Pata Amiga" width="120" />O paginas/ colado na frente é a assinatura desse erro. Dois pontinhos resolvem
— eles significam “sobe um nível”:
-<link rel="stylesheet" href="css/estilo.css" />
-<img src="img/logo.png" alt="Logo da Clínica Pata Amiga" width="120" />
+<link rel="stylesheet" href="../css/estilo.css" />
+<img src="../img/logo.png" alt="Logo da Clínica Pata Amiga" width="120" />A barra inicial resolve, até o dia em que quebra
A outra correção é a barra na frente: /css/estilo.css sempre parte da raiz do
site, não importa em que pasta a página esteja. A página de contato usa essa
forma e funciona:
<link rel="stylesheet" href="/css/estilo.css" />
<img src="/img/logo.png" alt="Logo da Clínica Pata Amiga" width="120" />Agora publique a mesma pasta dentro de uma subpasta, como o GitHub Pages faz
com usuario.github.io/pata-amiga. Copiei o site para publicado/pata-amiga e
servi a pasta de cima:
mkdir publicado && cp -R site publicado/pata-amiga
node servidor.js publicadoNenhum arquivo mudou de lugar dentro do projeto, e mesmo assim tudo caiu. A
barra inicial mandou o navegador procurar em /css/, quando o site inteiro
agora mora em /pata-amiga/css/. É por isso que “funcionava no meu servidor
local e quebrou no GitHub Pages” é uma frase tão comum: o local serve na raiz,
o Pages serve numa subpasta.
Dois cliques no arquivo: o 404 que nem chega a existir
Quem abre o HTML dando dois cliques não está falando com servidor nenhum — está
no protocolo file://. A mensagem muda, e é bom saber ler as duas:
node abrir.js "file:///private/tmp/pata-amiga/index.html"net::ERR_FILE_NOT_FOUND no lugar de 404 quer dizer: ninguém respondeu, o
navegador foi olhar no disco e não achou. A causa é a mesma — caminho errado.
Pior é a página de contato, aquela da barra inicial, aberta por dois cliques:
A barra inicial virou a raiz do disco, não a raiz do site. Por isso a recomendação de sempre estudar HTML com um servidor local rodando, nem que seja o de vinte linhas lá de cima.
“Na minha máquina funciona”: a caixa do nome do arquivo
Esta é a causa que mais assusta, porque o erro só aparece depois do deploy. A
página de equipe pede Logo.PNG; o arquivo no disco se chama logo.png.
Para não depender do navegador, pedi as três grafias direto ao servidor:
const base = 'http://localhost:5175';
for (const caminho of ['/img/logo.png', '/img/Logo.PNG', '/CSS/estilo.css']) {
const resposta = await fetch(base + caminho);
console.log(`${resposta.status} ${caminho}`);
}ls site/img
node servidor.js site > servidor.log 2>&1 & # o log do servidor sai da frente
sleep 1
node checar.mjsNo macOS, com o disco formatado como o Mac vem de fábrica:
Três grafias diferentes, três respostas 200. Agora a mesma pasta e o mesmo
servidor dentro de um container Debian. O cp no começo é essencial: ele copia
os arquivos para o disco do Linux, porque a pasta compartilhada com o Mac
continua se comportando como o Mac.
# dentro.sh, executado dentro do container
cp -r /mnt/site /site
cp /mnt/servidor.js /mnt/checar.mjs /
grep PRETTY_NAME /etc/os-release
echo "node $(node -v)"
ls /site/img
echo "--- Linux: mesma pasta, tres grafias ---"
node /servidor.js site > /servidor.log 2>&1 &
sleep 1
node /checar.mjsdocker run --rm -v "$PWD:/mnt:ro" node:24-slim bash /mnt/dentro.shNada no seu código mudou. O que mudou foi o sistema de arquivos: o APFS do Mac e
o NTFS do Windows guardam a caixa das letras mas ignoram ela na hora de
procurar; o ext4 do Linux, que roda em praticamente todo servidor, não ignora.
Logo.PNG e logo.png são dois arquivos diferentes lá.
O mesmo mecanismo derruba import no back-end — é uma das causas do
Cannot find module no Node.
A extensão que você não vê
A pessoa digita banner.png na hora de salvar, mas o programa gravou um JPEG e
acrescentou a extensão de verdade no fim. O Finder e o Explorador de Arquivos
escondem a última extensão e continuam mostrando banner.png. No terminal, o
nome verdadeiro aparece:
ls -l site/img
# um -o para cada URL, senão a segunda imagem vaza no terminal
curl -s -o /dev/null -o /dev/null -w '%{http_code} %{url_effective}\n' \
http://localhost:5175/img/banner.png \
http://localhost:5175/img/banner.png.jpgbanner.png.jpg é o nome real. Sempre confira pelo terminal, ou ligue a
exibição de extensões no Finder e no Explorador de Arquivos — no Mac, em
Finder › Configurações › Avançado.
O arquivo existe aqui e não chegou lá
Quando o caminho está certo, a caixa está certa, e mesmo assim o servidor
responde 404, falta perguntar se o arquivo foi mesmo publicado. Um .gitignore
herdado de outro projeto é o suspeito número um. O desta pasta tem três linhas
— *.log, node_modules/ e img/ — e a terceira entrou sem ninguém reparar:
git add .
git status --short
git check-ignore -v img/logo.pngO HTML e o CSS entraram no commit; a pasta img/ não aparece na lista. O
git check-ignore -v entrega o culpado com endereço: linha 3 do .gitignore,
regra img/. Sem esse comando, você passaria a tarde relendo caminhos que
estavam corretos.
O 404 que virou 200 e sumiu do console
Existe um cenário pior que o 404: o servidor configurado para devolver o
index.html em qualquer caminho desconhecido, o famoso fallback de aplicação de
página única. Copiei o servidor.js para um servidor-spa.js e troquei nele só
o catch (mais a mensagem de inicialização, para não confundir um servidor com
o outro):
} catch {
// fallback de SPA: qualquer caminho desconhecido devolve o index
res.writeHead(200, { 'content-type': 'text/html' });
res.end(await readFile(join(RAIZ, 'index.html')));
console.log(`200 ${req.url} (fallback para index.html)`);
}O mesmo index.html quebrado, agora servido por esse servidor:
node servidor-spa.js site # um terminal
node abrir.js http://localhost:5175/ # o outroConsole do navegador: vazio. Nenhum erro — o segundo terminal não imprimiu uma
linha sequer. Sem X-Content-Type-Options: nosniff na resposta, o Chrome nem
reclama do CSS que chegou como text/html: ele farejou o conteúdo e desistiu em
silêncio.
Para saber se a página realmente carregou, acrescentei ao abrir.js — antes do
close() — uma pergunta ao próprio Chrome: qual é a cor de fundo do body,
quantas regras de CSS chegaram e qual a largura real de cada imagem.
const laudo = await pagina.evaluate(() => ({
fundoDoBody: getComputedStyle(document.body).backgroundColor,
regrasCss: [...document.styleSheets].reduce((n, f) => n + (f.cssRules?.length ?? 0), 0),
imagens: [...document.images].map((i) => `${i.getAttribute('src')} -> ${i.naturalWidth}px`),
}));
console.log(laudo);Zero regras de CSS, imagens com zero pixel de largura: a página está tão
quebrada quanto antes, só que sem aviso nenhum. Com os caminhos corrigidos e o
servidor normal de volta, o mesmo laudo — o favicon.ico continua reclamando, e
continua não sendo problema seu:
naturalWidth valendo 0 é o teste mais honesto que existe para imagem que não
carregou — funciona até quando o console está limpo.
Um script que confere os caminhos antes de você publicar
O Chrome só reclama do que ele pediu, e na sua máquina ele nunca vai reclamar de
Logo.PNG. Então escrevi um verificador que lê cada HTML da pasta, resolve cada
src e href, e compara com o nome exato do arquivo no disco — inclusive a
caixa das letras:
import { readdir, readFile } from 'node:fs/promises';
import { basename, dirname, join, relative, resolve } from 'node:path';
const RAIZ = resolve(process.argv[2] ?? 'site');
const EXTERNO = /^(https?:|data:|mailto:|tel:|#|\/\/)/;
async function paginas(pasta) {
const lista = [];
for (const item of await readdir(pasta, { withFileTypes: true })) {
const caminho = join(pasta, item.name);
if (item.isDirectory()) lista.push(...(await paginas(caminho)));
else if (item.name.endsWith('.html')) lista.push(caminho);
}
return lista;
}
let quebrados = 0;
for (const pagina of await paginas(RAIZ)) {
console.log(`\n${relative(RAIZ, pagina)}`);
const html = await readFile(pagina, 'utf8');
for (const [, ref] of html.matchAll(/(?:src|href)="([^"]+)"/g)) {
if (EXTERNO.test(ref)) continue;
const alvo = ref.startsWith('/') ? join(RAIZ, ref) : resolve(dirname(pagina), ref);
const vizinhos = await readdir(dirname(alvo)).catch(() => null);
const nome = basename(alvo);
if (!vizinhos) {
quebrados++;
console.log(` 404 ${ref} — a pasta ${relative(RAIZ, dirname(alvo))} não existe`);
} else if (vizinhos.includes(nome)) {
console.log(` ok ${ref}`);
} else {
quebrados++;
const caixa = vizinhos.find((v) => v.toLowerCase() === nome.toLowerCase());
const pista = caixa
? `existe como ${caixa}: só muda a caixa`
: `não existe em ${relative(RAIZ, dirname(alvo)) || '.'}`;
console.log(` 404 ${ref} — ${pista}`);
}
}
}
console.log(`\n${quebrados} caminho(s) que o navegador não vai encontrar.`);
process.exit(quebrados ? 1 : 0);Rodando na pasta com os defeitos deste artigo:
node conferir-caminhos.mjs sitepaginas/contato.html ok /css/estilo.css ok /img/logo.png
paginas/equipe.html ok ../css/estilo.css 404 ../img/Logo.PNG — existe como logo.png: só muda a caixa
paginas/servicos.html ok ../css/estilo.css ok ../img/logo.png
4 caminho(s) que o navegador não vai encontrar.
A linha do Logo.PNG é a que paga o script: ela acusa, no Mac, o erro que só
apareceria em produção. E como o script sai com código 1 quando encontra
problema, ele serve de trava no seu npm run build ou no CI. Na pasta
corrigida, a saída termina em 0 caminho(s) que o navegador não vai encontrar.
O roteiro de três perguntas
| o que você vê | o que isso indica | onde confirmar |
|---|---|---|
404 no console e no log do servidor |
o caminho pedido não existe no servidor | compare o endereço do console com a árvore de pastas |
404 só depois do deploy |
caixa do nome do arquivo, ou arquivo não publicado | rode o site num container Linux e confira o git status |
net::ERR_FILE_NOT_FOUND |
você abriu por file://, sem servidor |
suba um servidor local e recarregue |
status 200 e página sem estilo |
fallback do servidor devolvendo o index | veja o Content-Type na aba Network |
404 que você não escreveu |
pedido automático do navegador (favicon.ico) |
ignore, ou adicione o ícone |
Na prática, três perguntas resolvem quase todos os casos: qual endereço o
navegador pediu (console), qual arquivo existe de verdade (ls no
terminal), e de onde a página estava sendo servida quando pediu (a URL na
barra de endereços). Quando as três respostas batem, o 404 acabou.
O que fazer agora
Suba um servidor local antes de continuar estudando — dá para usar
o servidor HTTP do Node sem framework nenhum ou
a extensão Live Server do VS Code. Depois, revise como o atributo src e o
href montam esses endereços em
imagens em HTML e em
links em HTML, e entenda os números que o servidor
devolve em
métodos HTTP e status code.
Se quiser ver o outro lado — o que o navegador faz com o HTML depois que os arquivos chegam —, leia como o navegador monta a árvore DOM. O caminho completo do assunto está no guia de HTML.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
O caminho da imagem dentro do CSS é o mesmo do HTML?
Nome de arquivo com espaço ou acento pode causar 404?
Um 404 quebra o resto da página?
Como faço uma página de 404 personalizada?
Corrigi o caminho e continua dando 404. O que falta?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 no macOS, Node 24.19.0 em Debian 12 e Chrome 151.0.7922.170, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — 404 Not Found — developer.mozilla.org
- MDN — What is a URL: absolute and relative URLs — developer.mozilla.org
- Apple Developer — files and directories — developer.apple.com



