Como Corrigir Erros "Cannot GET" Após o Deploy de uma SPA
Corrija erros Cannot GET e 404 de SPA após o deploy com rewrites de servidor para Nginx, Apache, Netlify, Vercel e S3 CloudFront.
Um erro “Cannot GET /route” ou 404 após o deploy de uma single-page app normalmente é um problema de configuração do servidor, e não um bug de roteamento. A correção consiste em fazer o servidor retornar o index.html para qualquer caminho de requisição que não corresponda a um arquivo real.
O padrão é familiar. O build vai ao ar, todas as páginas funcionam quando você navega clicando por elas, e então alguém dá um refresh em /dashboard, ou abre um link compartilhado para /orders/42, e recebe um 404 seco. Normalmente o router está correto e o build também. O servidor foi consultado por um arquivo que não existe.
Este artigo explica por que o erro só aparece em navegações completas (hard navigations), e depois aborda a correção e a configuração para Nginx, Apache, Netlify, Vercel e S3 atrás do CloudFront, junto com o efeito colateral que precisa ser tratado em seguida.
Principais Pontos
- Um 404 em uma SPA ao dar refresh acontece porque a requisição chega ao servidor, que procura um arquivo real naquele caminho e encontra apenas o
index.htmlna raiz. - Servidores de desenvolvimento local escondem o bug porque já fazem fallback para o
index.htmlem caminhos sem correspondência. - A correção é um rewrite, não um redirect: sirva o
index.htmlcom status 200 para que a URL permaneça intacta e o router possa lê-la. - No S3, a abordagem com error document mantém o status 404; é uma custom error response do CloudFront mapeando tanto 403 quanto 404 para
/index.htmlque retorna um 200. - Um fallback catch-all faz com que URLs inválidas retornem 200, então a aplicação precisa da sua própria rota wildcard renderizando uma view de “não encontrado”.
Quando o Erro “Cannot GET” Aparece?
O erro aparece apenas em navegações completas: um refresh da página, uma URL digitada na barra de endereços ou um deep link compartilhado aberto em uma nova aba. A navegação interna da aplicação continua funcionando, porque, uma vez que a aplicação carregou, o router troca as views inteiramente no navegador, sem contatar o servidor. A mensagem exata varia conforme o host: servidores baseados em Express exibem “Cannot GET /route”, enquanto hosts estáticos retornam sua página 404.
É também por isso que o bug sobrevive ao QA. Session replays de uma SPA recém-deployada mostram a quebra em navegações completas — um refresh ou um link aberto externamente — nunca durante cliques dentro da aplicação. Assim, testes que apenas clicam pela aplicação em execução passam sem problemas, enquanto usuários reais batem no 404.
Por Que um 404 de SPA ao Dar Refresh É um Problema do Servidor?
Um servidor estático mapeia cada caminho de requisição para um arquivo em disco. Um build de SPA produz um único arquivo HTML, o index.html, além dos assets de JS e CSS. Portanto, uma requisição direta para /dashboard não encontra arquivo algum naquele caminho e o servidor corretamente responde 404. React Router, Vue Router e SvelteKit configurado como single-page app todos enfrentam isso de forma idêntica, porque o framework é irrelevante: as rotas existem apenas no JavaScript que ainda não foi carregado.
O erro nunca aparece no desenvolvimento local porque a maioria dos dev servers de SPA já vem com o fallback habilitado: qualquer caminho que não corresponda a um arquivo recebe o index.html automaticamente. Seu ambiente local estava silenciosamente fazendo aquilo que seu servidor de produção não faz.
Qual É a Correção para um Erro “Cannot GET”?
Configure o servidor para servir o index.html em qualquer caminho de requisição que não corresponda a um arquivo existente, de modo que a aplicação carregue e seu router renderize a view daquela URL. Isso precisa ser um rewrite retornando o index.html com status 200, não um redirect: um redirect mudaria a URL na barra de endereços, e o router precisa do caminho original intacto.
| Host | Onde a configuração fica | Mecanismo |
|---|---|---|
| Nginx | bloco server | try_files |
| Apache | vhost ou .htaccess | FallbackResource |
| Netlify | _redirects ou netlify.toml | regra de rewrite com status 200 |
| Vercel | vercel.json | array rewrites |
| S3 + CloudFront | configuração de website do bucket + distribution | error document + custom error response |
Se você realmente não puder mexer no servidor, o roteamento baseado em hash contorna tudo isso porque o fragmento nunca sai do navegador, mas isso transforma permanentemente cada URL em /#/about, então trate essa opção como último recurso.
Nginx e Apache
Nginx e Apache expressam o fallback da SPA como uma única diretiva na configuração do servidor. No Nginx, adicione um fallback com try_files na location raiz. Ele procura o caminho da requisição como arquivo, depois como diretório, e quando nenhum dos dois aparece serve o /index.html internamente com status 200:
server {
listen 80;
root /var/www/app/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
No Apache, uma única diretiva do mod_dir faz o mesmo trabalho. Arquivos reais continuam sendo entregues como eles mesmos, e todo o resto cai no fallback:
FallbackResource /index.html
Se a aplicação vive sob um sub-path, inclua-o: FallbackResource /app/index.html. O equivalente mais antigo com mod_rewrite ainda funciona no .htaccess:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]
Netlify e Vercel
No Netlify, uma regra de redirect com status 200 se torna um rewrite: o navegador continua exibindo o caminho que o visitante pediu, e o conteúdo do index.html volta na resposta. Você pode adicionar um arquivo _redirects de uma linha:
/* /index.html 200
ou o equivalente no netlify.toml:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
O arquivo _redirects precisa terminar dentro do diretório de publicação, então garanta que seu build o copie para a pasta de output; o netlify.toml fica na raiz do repositório. Uma regra splat não assume o controle de um caminho que tenha um arquivo real por trás, então os assets de JS e CSS continuam carregando.
Para o Vercel, adicione uma entrada rewrites ao vercel.json:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
Prefira o destino explícito /index.html em vez de /: ambos resolvem para o mesmo arquivo no Vercel, mas a forma explícita declara o que é realmente servido e se transfere como modelo mental para qualquer outro host. Uma exceção: com cleanUrls: true definido, o destino não pode carregar a extensão .html, e o Vercel mapeia index.html para a raiz do site, então defina o destino como /.
S3 e CloudFront
Corrigir um 404 de SPA no S3 exige duas partes de configuração, porque a configuração do bucket, isolada, preserva o status de erro. No S3 static website hosting, defina index.html tanto como index document quanto como error document:
aws s3 website s3://your-bucket \
--index-document index.html \
--error-document index.html
Isso serve a aplicação para caminhos desconhecidos, mas preserva o status de erro: o navegador recebe o index.html com código 404. Para retornar 200, adicione custom error responses do CloudFront mapeando tanto 403 quanto 404 para /index.html com response code 200. O mapeamento do 403 importa porque uma distribution que usa o endpoint REST do S3 como origin recebe 403 Access Denied, e não 404, para chaves que não existem. Em termos de Terraform:
custom_error_response {
error_code = 403
response_code = 200
response_page_path = "/index.html"
}
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
}
Uma armadilha: custom error responses se aplicam a toda a distribution, então, se você faz proxy de /api/* pela mesma distribution, os 403 e 404 da API também voltarão como index.html.
O Custo: Seus 404s Reais Desaparecem
Um fallback catch-all tem um preço: URLs genuinamente incorretas agora retornam index.html com status 200 em vez de um 404 real. O servidor não consegue mais distinguir /orders/42 de /ordersss/42, então a aplicação precisa definir sua própria rota wildcard renderizando uma view de “não encontrado”. Todo router tem uma forma de escrever isso; no React Router fica assim:
<Route path="*" element={<NotFound />} />
Note que este é um 404 renderizado no cliente: o status HTTP continua sendo 200, o que importa se você se preocupa com a forma como os crawlers classificam essas páginas.
Conclusão
O 404 ao dar refresh é o servidor fazendo exatamente o que servidores estáticos fazem, e a correção é uma única regra aplicada no dialeto do seu host: reescrever todo caminho que não seja um arquivo para o index.html com status 200. Adicione o snippet do seu host, faça o redeploy, dê um hard refresh em uma rota profunda para confirmar e então adicione a rota wildcard de “não encontrado” para que URLs inválidas ainda informem aos usuários que eles se perderam.
FAQs
A correção com fallback de SPA funciona no GitHub Pages?
Não. O GitHub Pages não suporta rewrites no servidor, então não há como configurar uma regra de fallback para index.html. A solução padrão é uma página 404.html personalizada contendo um script que redireciona para o index.html preservando o caminho requisitado, que o router então restaura após o carregamento. O GitHub continua servindo essa página com status 404. A outra opção é o roteamento baseado em hash, que nunca envia a rota ao servidor.
Frameworks com renderização no servidor como Next.js ou Nuxt têm esse problema?
Não quando executam seu próprio servidor. Um framework com renderização no servidor trata todas as rotas no servidor, então um refresh ou deep link retorna HTML renderizado diretamente. O problema do 404 ao dar refresh afeta apenas builds estáticos de single-page em que as rotas existem exclusivamente no JavaScript do cliente. Uma aplicação exportada estaticamente a partir de um desses frameworks ainda pode enfrentar o problema quando uma rota requisitada não tem arquivo HTML pré-renderizado em disco.
Reescrever todos os caminhos para o index.html vai quebrar meus assets de JS e CSS?
Não. Cada mecanismo procura um arquivo real antes de recorrer ao fallback: o try_files do Nginx tenta primeiro a URI da requisição, o FallbackResource do Apache não interfere em requisições para arquivos reais, e um rewrite splat do Netlify não assume o controle de um caminho existente a menos que você force com 200!. Se os assets continuarem falhando após adicionar o fallback, a causa habitual são caminhos relativos de assets sendo resolvidos sob uma rota aninhada, fazendo o navegador requisitá-los do diretório errado e receber o index.html em vez disso.
Servir o index.html com status 200 prejudica o SEO?
Pode prejudicar. Quando uma URL inexistente retorna status 200 com conteúdo de 'não encontrado', o Google pode classificá-la como um 'soft 404' e removê-la do índice, porque o status code não distingue mais páginas reais de URLs inválidas. Se a indexação em buscadores importa para as suas rotas, pré-renderização ou renderização no servidor restauram os status codes corretos por rota. Para aplicações atrás de um login, os crawlers nunca veem as rotas, então esse trade-off é irrelevante.