Erro 404 ao recarregar uma SPA React/Vue depois do deploy

O servidor procura um arquivo na URL que você recarregou, não encontra e devolve 404. Resolva fazendo o servidor responder caminhos desconhecidos com o index.html, para o roteador do cliente assumir.

Atualizado em 6 min de leitura

Objetivos de aprendizado

Depois de ler este artigo, você vai conseguir:

  • Entender por que recarregar quebra rotas que funcionavam na navegação
  • Aplicar o fallback para o index.html no nginx, Express, Apache ou Netlify
  • Evitar que páginas 404 reais e arquivos faltando fiquem escondidos
Nesta página

Você faz o deploy do seu app React ou Vue. A página inicial carrega, os links funcionam, e aí você recarrega em /dashboard ou cola um link direto e recebe um 404 do servidor. Na sua máquina isso nunca aconteceu.

O app está certo. O servidor é que não sabe que /dashboard é uma tela do seu JavaScript, e não um arquivo. A seguir, o mecanismo e a correção para os servidores mais comuns.

CLIQUE EM UM LINKRECARREGAR A PÁGINANavegadorRoteadortroca a tela/ → /dashboardServidornão recebe nadaNavegadorrecarrega /dashboardGET /dashboardServidorprocura o arquivo404sem fallbackindex.htmlcom fallback
Clicar num link não chama o servidor; recarregar chama, e sem fallback para o index.html ele responde 404.

Por que recarregar dá 404 se a navegação funciona?#

Uma single-page application (SPA) entrega um único arquivo HTML, normalmente o index.html, mais os pacotes de JavaScript. Quando você clica num link, o roteador (React Router, Vue Router) usa a History API e troca a tela. A URL muda, mas o navegador não envia nenhuma requisição.

Recarregar é diferente. O navegador pede ao servidor exatamente aquele caminho:

http
GET /dashboard HTTP/1.1
Host: example.com

Uma hospedagem estática associa URLs a arquivos. Não existe arquivo nem pasta dashboard na saída do build, então ela responde:

text
404 Not Found

O servidor de desenvolvimento esconde isso porque já faz fallback para o index.html em caminhos desconhecidos. A documentação do Vite também avisa que o vite preview serve só para visualizar o build localmente e não foi feito para ser um servidor de produção, então um preview funcionando prova pouco sobre a sua hospedagem.

Como corrigir no servidor?#

Configure o servidor para devolver o index.html em qualquer caminho que não seja um arquivo existente. Assim o navegador carrega o app, o roteador lê a URL e mostra a tela certa. A documentação do Vue Router descreve isso como a configuração de servidor obrigatória no modo history.

A regra tem duas partes, nesta ordem:

  1. Se a requisição corresponde a um arquivo real (um script, uma folha de estilo, uma imagem), sirva o arquivo.
  2. Caso contrário, sirva o index.html e deixe o roteador do cliente decidir.

Veja essa regra nos servidores mais comuns.

nginx#

Use try_files no bloco location /:

nginx
server {
  listen 80;
  root /usr/share/nginx/html;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

O nginx tenta o arquivo e depois o diretório. Se nenhum dos dois existir, vale o último parâmetro, que aqui é um redirecionamento interno para /index.html. Recarregue o nginx depois de editar.

Express ou outro servidor Node.js#

Sirva a pasta do build e depois acrescente uma rota curinga que envia o index.html. Este exemplo usa o Express 5, que exige um curinga com nome:

js
import express from "express";
import path from "node:path";
import { fileURLToPath } from "node:url";

const dist = path.join(path.dirname(fileURLToPath(import.meta.url)), "dist");
const app = express();

app.use(express.static(dist));
app.get("/{*splat}", (req, res) => {
  res.sendFile(path.join(dist, "index.html"));
});

app.listen(process.env.PORT || 3000);

No Express 4 a rota curinga é app.get("*", ...). No Express 5 o curinga precisa ter nome, e /{*splat} também casa com o caminho raiz, como explica o guia de migração do Express. Registre a rota curinga depois do express.static e das rotas da sua API, senão ela engole essas rotas.

Apache#

Coloque isto num arquivo .htaccess ao lado do index.html, com o mod_rewrite ativado:

apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.html [L]
</IfModule>

As duas condições pulam arquivos e diretórios existentes, então só os caminhos desconhecidos caem no fallback.

Netlify e Firebase Hosting#

Na Netlify, adicione um arquivo _redirects na pasta publicada (num app Vite, coloque em public/ para ele ser copiado para dist/):

text
/*  /index.html  200

O 200 transforma a regra numa reescrita, e não num redirecionamento, então a URL continua a mesma. Por padrão, a Netlify não sobrepõe URLs que correspondem a um arquivo real, então os arquivos continuam sendo servidos normalmente.

No Firebase Hosting, adicione uma reescrita no firebase.json (arquivos que existem continuam sendo servidos primeiro):

json
{
  "hosting": {
    "public": "dist",
    "rewrites": [
      { "source": "**", "destination": "/index.html" }
    ]
  }
}

Outras plataformas têm uma configuração equivalente, muitas vezes chamada de "SPA mode", "rewrites" ou "fallback". Procure essas palavras na documentação da sua hospedagem.

Por que agora aparece uma página em branco ou um erro de MIME type?#

Um fallback amplo demais causa o problema oposto. Se o navegador pede /assets/app.abc123.js e o arquivo não existe, o servidor devolve o index.html com status 200, e o navegador recebe HTML onde esperava JavaScript. Com scripts de módulo, que é o que o Vite gera, o Chrome mostra o primeiro erro abaixo; com scripts clássicos, o segundo:

text
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html".
Uncaught SyntaxError: Unexpected token '<'

Causas comuns:

  • Caminho base errado. O app é servido num subcaminho (/app/), mas o build foi feito para /. Ajuste o base no Vite.
  • URLs relativas de arquivos em rotas profundas. Em /users/42, um src="assets/app.js" vira /users/assets/app.js. Use caminhos absolutos começando com /.
  • Deploy desatualizado. O index.html em cache aponta para arquivos com hash do build anterior, que não existem mais. Envie os arquivos novos junto com o index.html novo e evite cache longo no index.html.

Abra a aba Network e confira a requisição que falha. Se um arquivo .js volta como text/html, o fallback está pegando um arquivo que falta.

Como manter uma página 404 de verdade?#

Com um fallback amplo, o servidor nunca devolve 404 para páginas. A documentação do Vue Router aponta essa consequência e sugere uma rota curinga dentro da aplicação:

js
// Vue Router
{ path: "/:pathMatch(.*)", component: NotFound }
jsx
// React Router
<Route path="*" element={<NotFound />} />

O usuário vê uma tela de página não encontrada, mas o servidor continua respondendo 200, e buscadores podem tratar essas páginas como soft 404. Se isso importa, adicione uma meta tag noindex na tela de página não encontrada, ou passe para renderização no servidor ou pré-renderização, para que o servidor possa devolver um 404 real. Veja os prós e contras em SSR vs SSG vs estático.

E o HashRouter?#

O HashRouter (React) e o createWebHashHistory (Vue) colocam a rota depois do #, como em /#/dashboard. O navegador nunca envia o fragmento ao servidor, então todo recarregamento pede / e nenhuma configuração de servidor é necessária. É uma opção legítima para hospedagens em que você não consegue adicionar reescritas. A desvantagem é que as URLs ficam piores e o servidor não enxerga nem indexa as rotas individuais.

Como fazer isso na Square Cloud#

A Square Cloud é hospedagem self-service, então você decide como o build é servido. No runtime de sites estáticos, o fallback é um passo só: renomeie o index.html do build para vite.html. O runtime responde com o vite.html a todo caminho que não é um arquivo, então qualquer link direto carrega o app. Sem vite.html, caminhos desconhecidos recebem o seu 404.html.

Se preferir controlar o roteamento você mesmo, use o runtime de Node.js com um servidor Express como o de cima (com express no seu package.json). As duas opções estão no tutorial de React da documentação. Uma aplicação web precisa de pelo menos 512 MB de RAM do pool compartilhado do seu plano e ganha um subdomínio *.squareweb.app com HTTPS. O deploy pode ser por upload de ZIP no painel, pela CLI ou pelo GitHub.

Depois do deploy, abra um link direto e recarregue a página para confirmar a correção. O caminho completo do build até a URL no ar está em como hospedar um app React, e os recursos para sites estão na página de Sites.

Hospedagem de sites

Sites estáticos e SSR com TLS automático e CDN global

Saiba mais

Perguntas frequentes

Por que minha SPA funciona na página inicial mas dá 404 ao recarregar?

Clicar num link muda a URL no navegador sem fazer requisição, e o roteador cuida disso. Recarregar envia uma requisição de verdade para aquele caminho, e um servidor estático não tem arquivo nele.

O HashRouter resolve?

Sim. A rota fica depois do # na URL, que o navegador nunca envia ao servidor, então todo recarregamento pede o mesmo index.html. O preço são URLs mais feias, e o servidor não enxerga a rota.

O fallback deve responder com status 200 ou 404?

Numa SPA renderizada no cliente, 200 com o index.html, porque é a aplicação que decide o que mostrar. Mostre uma tela de página não encontrada dentro da aplicação para rotas desconhecidas.

Por que aparece uma página em branco ou um erro de MIME type em vez de 404?

Um fallback amplo demais também responde com o index.html a pedidos de arquivos JavaScript ou CSS que não existem. O navegador recebe HTML onde esperava um script. Confira os caminhos dos arquivos e a configuração de base.

Próximo artigoHospedagem de sites vs hospedagem de aplicações (Node, Python, bots)Hospedagem de site entrega arquivos prontos. Hospedagem de aplicações mantém um processo rodando. Veja a diferença, a tabela e qual escolher para cada projeto.