Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
Erro resolvidoIniciantecódigo testado

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.

Rodolfo Mori12 min de leitura

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:

html
<!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:

text
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.html

Os 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:

js
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:

js
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();
bash
node servidor.js site                  # um terminal
node abrir.js http://localhost:5175/   # o outro
[error] Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:5175/estilo.css [error] Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:5175/imagens/logo.png [error] Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:5175/img/fachada.jpeg [error] Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:5175/favicon.ico
servindo ./site em http://localhost:5175 200 / 404 /estilo.css 404 /imagens/logo.png 404 /img/fachada.jpeg 404 /favicon.ico

Repare 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.

página aberta: /paginas/servicos.html base = /paginas/ (o nome do arquivo é descartado) href="css/estilo.css" /paginas/css/estilo.css 404 href="../css/estilo.css" /css/estilo.css 200 href="/css/estilo.css" /css/estilo.css 200

A página de serviços mora em paginas/, e copiou o caminho do index.html sem pensar:

html
<link rel="stylesheet" href="css/estilo.css" />
<img src="img/logo.png" alt="Logo da Clínica Pata Amiga" width="120" />
[error] Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:5175/paginas/css/estilo.css [error] Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:5175/paginas/img/logo.png [error] Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:5175/favicon.ico
servindo ./site em http://localhost:5175 200 /paginas/servicos.html 404 /paginas/css/estilo.css 404 /paginas/img/logo.png 404 /favicon.ico

O paginas/ colado na frente é a assinatura desse erro. Dois pontinhos resolvem — eles significam “sobe um nível”:

diff
-<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" />
servindo ./site em http://localhost:5175 200 /paginas/servicos.html 200 /css/estilo.css 200 /img/logo.png 404 /favicon.ico

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:

html
<link rel="stylesheet" href="/css/estilo.css" />
<img src="/img/logo.png" alt="Logo da Clínica Pata Amiga" width="120" />
servindo ./site em http://localhost:5175 200 /paginas/contato.html 200 /img/logo.png 200 /css/estilo.css 404 /favicon.ico

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:

bash
mkdir publicado && cp -R site publicado/pata-amiga
node servidor.js publicado
servindo ./publicado em http://localhost:5175 200 /pata-amiga/paginas/contato.html 404 /css/estilo.css 404 /img/logo.png 404 /favicon.ico

Nenhum 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:

bash
node abrir.js "file:///private/tmp/pata-amiga/index.html"
[error] Failed to load resource: net::ERR_FILE_NOT_FOUND file:///private/tmp/pata-amiga/estilo.css [error] Failed to load resource: net::ERR_FILE_NOT_FOUND file:///private/tmp/pata-amiga/imagens/logo.png [error] Failed to load resource: net::ERR_FILE_NOT_FOUND file:///private/tmp/pata-amiga/img/fachada.jpeg

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:

[error] Failed to load resource: net::ERR_FILE_NOT_FOUND file:///css/estilo.css [error] Failed to load resource: net::ERR_FILE_NOT_FOUND file:///img/logo.png

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:

js
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}`);
}
bash
ls site/img
node servidor.js site > servidor.log 2>&1 &   # o log do servidor sai da frente
sleep 1
node checar.mjs

No macOS, com o disco formatado como o Mac vem de fábrica:

banner.png.jpg fachada.jpg logo.png 200 /img/logo.png 200 /img/Logo.PNG 200 /CSS/estilo.css

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.

bash
# 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.mjs
bash
docker run --rm -v "$PWD:/mnt:ro" node:24-slim bash /mnt/dentro.sh
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" node v24.19.0 banner.png.jpg fachada.jpg logo.png --- Linux: mesma pasta, tres grafias --- 200 /img/logo.png 404 /img/Logo.PNG 404 /CSS/estilo.css

Nada 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:

bash
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.jpg
total 24 -rw-r--r--@ 1 rodolfomori wheel 150 Aug 22 17:22 banner.png.jpg -rw-r--r--@ 1 rodolfomori wheel 150 Aug 22 17:18 fachada.jpg -rw-r--r--@ 1 rodolfomori wheel 82 Aug 22 17:18 logo.png 404 http://localhost:5175/img/banner.png 200 http://localhost:5175/img/banner.png.jpg

banner.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:

bash
git add .
git status --short
git check-ignore -v img/logo.png
A .gitignore A css/estilo.css A index.html A paginas/contato.html A paginas/equipe.html A paginas/servicos.html .gitignore:3:img/ img/logo.png

O 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):

js
  } 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:

bash
node servidor-spa.js site              # um terminal
node abrir.js http://localhost:5175/   # o outro
SPA fallback em http://localhost:5175 200 / 200 /estilo.css (fallback para index.html) 200 /imagens/logo.png (fallback para index.html) 200 /img/fachada.jpeg (fallback para index.html) 200 /favicon.ico (fallback para index.html)

Console 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.

js
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);
{ fundoDoBody: 'rgba(0, 0, 0, 0)', regrasCss: 0, imagens: [ 'imagens/logo.png -> 0px', 'img/fachada.jpeg -> 0px' ] }

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:

[error] Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:5175/favicon.ico { fundoDoBody: 'rgb(15, 26, 20)', regrasCss: 1, imagens: [ 'img/logo.png -> 4px', 'img/fachada.jpg -> 8px' ] }

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:

js
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:

bash
node conferir-caminhos.mjs site
index.html 404 estilo.css — não existe em . 404 imagens/logo.png — a pasta imagens não existe 404 img/fachada.jpeg — não existe em img

paginas/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.

Ver todos os vídeos do canal
  • html
  • 404
  • caminho de arquivo
  • deploy
  • erro

Perguntas frequentes

O caminho da imagem dentro do CSS é o mesmo do HTML?
Não. Um url() dentro de um arquivo .css é resolvido a partir da pasta do próprio CSS, não da página que carregou ele. Com o CSS em css/estilo.css e a imagem em img/fundo.jpg, o caminho certo é url(../img/fundo.jpg), mesmo que no HTML você escreva img/fundo.jpg.
Nome de arquivo com espaço ou acento pode causar 404?
Pode. O navegador envia espaço como %20 e acento numa sequência codificada; se o servidor gravou o nome com outra codificação, os dois não batem. Nomeie arquivo em minúsculas, sem espaço e sem acento — é a convenção que elimina a categoria inteira de problema.
Um 404 quebra o resto da página?
Depende do que faltou. Imagem sem carregar mostra o texto do alt e o resto continua. CSS sem carregar deixa a página sem estilo, mas ela funciona. Já um script que dá 404 derruba todo código que dependia dele, e você vê um segundo erro logo abaixo, do tipo "x is not defined".
Como faço uma página de 404 personalizada?
No GitHub Pages e na Netlify, basta um arquivo 404.html na raiz do site publicado — eles servem esse arquivo quando o caminho não existe. Em servidor próprio, é configuração: no Nginx é a diretiva error_page, e num servidor em Node é o que você escreve dentro do catch.
Corrigi o caminho e continua dando 404. O que falta?
Quase nunca é cache: 404 sem cabeçalho de validação não é guardado, e um F5 comum já refaz o pedido. Confira três coisas, nesta ordem: qual endereço chegou ao servidor (o log, não o console); de qual pasta o servidor está servindo, porque às vezes ele aponta para a pasta errada do projeto; e, se o projeto tem build, se o arquivo foi para a pasta publicada e não só para a pasta do código-fonte.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — 404 Not Found — developer.mozilla.org
  2. MDN — What is a URL: absolute and relative URLs — developer.mozilla.org
  3. Apple Developer — files and directories — developer.apple.com

Continue por aqui