Skip to content

fix(error-handler): auto-reload em chunk load errors após deploy (stale chunks)#600

Merged
adm01-debug merged 1 commit into
mainfrom
fix/chunk-load-error-auto-reload
Jun 2, 2026
Merged

fix(error-handler): auto-reload em chunk load errors após deploy (stale chunks)#600
adm01-debug merged 1 commit into
mainfrom
fix/chunk-load-error-auto-reload

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented Jun 2, 2026

🚨 Bug observado em produção (2026-06-02 11:54-11:57 BRT)

Após o merge do PR #599 e o consequente deploy do Vercel, usuários com tabs já abertas começaram a ver falhas de navegação em todas as rotas lazy-loaded:

FavoritesPage-BEkuRGc_.js:1
  Failed to load module script: Expected a JavaScript-or-Wasm module script
  but the server responded with a MIME type of "text/html".

TypeError: Failed to fetch dynamically imported module:
  https://www.promogifts.com.br/assets/ProductDetail-BZ9BghlM.js

Rotas afetadas: /favoritos, /produto/:id, /simulador-precos, /colecoes, /mockup-generator, e qualquer rota com lazy(() => import(...)).

Por que aconteceu

Quando o Vercel publicou os novos chunks, os hashes mudaram:

  • antigo: ProductDetail-BZ9BghlM.js404
  • novo: ProductDetail-Xy7AbCdE.js

O cliente, ainda com index.html antigo em memória, tentou buscar o chunk antigo. Vercel respondeu com o index.html (SPA fallback, 200), e o browser recusou: arquivo .js veio com text/html.

Solução

useGlobalErrorCatcher já capturava unhandledrejection mas só mostrava o toast genérico "Erro inesperado. Tente recarregar a página." — perdendo a chance de fazer o reload automaticamente.

Este PR adiciona detecção específica de chunk error seguida de auto-reload protegido contra loop:

  1. isChunkLoadError(error) — pattern-matcher contra 7 assinaturas conhecidas (Chrome/Firefox/Safari) + fallback heurístico baseado em stack contendo /assets/*.js
  2. handleChunkLoadError() — tenta 1 (e apenas 1) reload por sessão, usando sessionStorage como guard. Se já tentou, mostra toast com instrução de hard refresh manual.
  3. useGlobalErrorCatcher — agora intercepta chunk errors em ambos os listeners (error para falhas de tag <script>, unhandledrejection para import() que rejeita) antes do toast genérico.
  4. lazyWithRetry() (bônus) — wrapper opcional para React.lazy() que tenta 1 retry com 800ms de delay. Não consumido neste PR, disponível para uso futuro em rotas problemáticas.

Padrões cobertos pelo isChunkLoadError

Padrão Browser Observado em produção
Failed to fetch dynamically imported module Chrome/Edge ✅ sim
Failed to load module script Chrome/Edge ✅ sim
Expected a JavaScript-or-Wasm module script Chrome/Edge ✅ sim
error loading dynamically imported module Firefox preventivo
Importing a module script failed Safari preventivo
Loading chunk \w+ failed Webpack legacy preventivo
ChunkLoadError Webpack named preventivo
TypeError + stack com /assets/*.js qualquer fallback heurístico

Cenários simulados

Cenário Resultado esperado
Deploy novo + tab antiga clica em rota lazy Toast "Nova versão disponível, atualizando…" + reload após 1.5s
Reload conclui com sucesso → nova tentativa de nav sessionStorage limpa após 5s, contador zerado
Reload falha (deploy realmente quebrado) Toast com instrução "Ctrl+Shift+R", sem segundo reload automático
Erro normal (não chunk) Comportamento original preservado: log + toast genérico
sessionStorage indisponível (Safari private) try/catch silencia, segue para o reload

Arquivos alterados

  • src/hooks/ui/useErrorHandler.ts — adicionados isChunkLoadError, handleChunkLoadError, lazyWithRetry, e branch nos handlers do useGlobalErrorCatcher

Bugs vistos no console que não são tratados neste PR

  • ⚠️ Blocked aria-hidden on an element because its descendant retained focus — Radix Dialog/Sheet aplicando aria-hidden no <div.min-h-screen> enquanto <main> retém foco. Issue separada.
  • ⚠️ Tooltip is changing from uncontrolled to controlled — algum Tooltip alternando open entre undefined e boolean. Issue separada.

Esses são bugs de acessibilidade/warnings, não bloqueiam UX. Vão em PRs próprios após identificar os componentes específicos.

Pós-merge

Após o merge deste PR, o Vercel vai fazer um novo deploy. Usuários com tabs antigas (incluindo as deste novo deploy) vão receber o auto-reload uma única vez e seguir na nova versão. A partir daí, todos os deploys subsequentes terão essa proteção embutida.


Summary by cubic

Detecta erros de carregamento de chunks após deploy e faz auto-reload uma única vez para evitar falhas de navegação em rotas lazy. Usuários com abas antigas passam a atualizar para a nova versão sem loops.

  • Bug Fixes
    • isChunkLoadError: identifica padrões de erro de chunks em Chrome/Firefox/Safari.
    • Auto-reload controlado por sessionStorage (1 tentativa por sessão); se falhar, mostra instrução de hard refresh.
    • useGlobalErrorCatcher: intercepta erros em error e unhandledrejection antes do toast genérico.
    • lazyWithRetry: wrapper opcional para React.lazy() com 1 retry em erro de chunk (não utilizado neste PR).

Written for commit 9a713d1. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features
    • Detecção automática e tratamento melhorado de erros relacionados a deploys, com recarregamento automático da aplicação para garantir que o usuário tenha acesso às versões mais recentes dos recursos carregados.
    • Mensagens de erro mais claras e instruções de refresh manual quando necessário.

…load

Adds detection of "Failed to fetch dynamically imported module" and
related stale-chunk errors that happen when Vercel publishes new asset
hashes while users have older tabs open.

- isChunkLoadError(error): pattern-matcher for 7 known chunk error
  signatures + stack-based fallback for unnamed TypeErrors
- handleChunkLoadError: triggers 1 (and only 1) auto-reload per session,
  guarded by sessionStorage to prevent infinite loops
- lazyWithRetry: optional wrapper for React.lazy with 1 retry on chunk
  error (for future use in route components)
- useGlobalErrorCatcher: now intercepts chunk errors in both 'error'
  and 'unhandledrejection' events before they reach the generic toast
Copilot AI review requested due to automatic review settings June 2, 2026 12:01
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
we-dream-big Ready Ready Preview, Comment Jun 2, 2026 12:02pm

@supabase
Copy link
Copy Markdown

supabase Bot commented Jun 2, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Nova infraestrutura em useErrorHandler.ts detecta stale deploy / chunk load errors via padrões de mensagem e stack trace, com retry automático controlado por sessionStorage. Quando identificado, supprime toast genérico, executa reload único por sessão com mensagens específicas, e oferece helpers isChunkLoadError e lazyWithRetry para reutilização em componentes lazy.

Changes

Detecção e recuperação de stale deploy / chunk load error

Layer / File(s) Summary
Detecção de chunk load error e helpers exportados
src/hooks/ui/useErrorHandler.ts
Padrões regex e fallback por stack trace identificam stale deploy errors. Funções isChunkLoadError e lazyWithRetry exportadas. Lógica de sessionStorage rastreia tentativas e evita loops, com reload automático única vez por sessão, grace period para resetar contador, e mensagens toast específicas.
Integração nos handlers de erro global e cleanup
src/hooks/ui/useErrorHandler.ts
useGlobalErrorCatcher detecta chunk load errors em handlers de window error e unhandledrejection, aciona preventDefault, dispara reload automático e bypassa toast genérico para esse caso. Cleanup cancela explicitamente clearTimer do grace period.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Rationale: Lógica de detecção com padrões regex, gerenciamento de estado em sessionStorage, controle de timers e cleanup, integração dupla em handlers, uso de event.preventDefault, e necessidade de validar edge cases (loops de reload, race conditions com grace period, diferenças entre error vs unhandledrejection).

Possibly related PRs

  • adm01-debug/promo-gifts-v4#42: Resolve problema de rotas profundas retornando HTML em vez de chunks via vercel.json, cenário raiz que this PR mitiga com detecção/recuperação automática.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título descreve com precisão a mudança principal: implementação de auto-reload automático para chunk load errors após deploy, com menção específica do problema (stale chunks).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chunk-load-error-auto-reload

Comment @coderabbitai help to get the list of available commands and usage tips.

@adm01-debug adm01-debug merged commit 16d86c4 into main Jun 2, 2026
27 of 39 checks passed
@adm01-debug adm01-debug deleted the fix/chunk-load-error-auto-reload branch June 2, 2026 12:04
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a713d1d55

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +75 to +77
} catch {
// sessionStorage indisponível (Safari private etc) — silencia
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the reload guard when storage is blocked

When this catch path is hit (for example in a sandboxed frame or a browser/privacy mode that denies sessionStorage writes), the reload attempt is never persisted; getReloadAttempts() will keep returning 0, so any persistent chunk failure is treated as a first attempt and schedules another automatic reload. That defeats the loop protection this handler is meant to provide and can trap affected users in a reload loop instead of showing the manual hard-refresh message.

Useful? React with 👍 / 👎.

Comment on lines +207 to +209
const clearTimer = window.setTimeout(() => {
clearReloadAttempts();
}, CHUNK_RELOAD_CLEAR_MS);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not clear attempts before lazy chunks are proven healthy

Because this timer clears the guard merely 5 seconds after the shell boots, a deploy with a broken lazy route that is not requested during those first 5 seconds will lose the previous auto-reload count. If the user later navigates to that route, the same persistent chunk error is treated as a fresh first attempt and auto-reloads again instead of stopping after one session attempt, so the advertised broken-deploy fallback can still loop on delayed route access.

Useful? React with 👍 / 👎.

Comment on lines +55 to +57
error instanceof TypeError &&
typeof error.stack === 'string' &&
/\/assets\/[^/]+\.js/.test(error.stack)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow the TypeError stack fallback

In production, ordinary application TypeErrors also have stack frames pointing at bundled files under /assets/*.js, so this fallback will classify many real runtime bugs (for example a null dereference inside a lazy page) as stale chunk failures. Those errors then skip the normal global error logging/toast path and trigger an automatic reload, masking the original bug and disrupting users even when no deploy-stale chunk is involved.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds client-side protection against “stale chunk” failures after deploys by detecting known chunk-load error signatures and triggering a guarded auto-reload from the global error listeners, reducing navigation breakage for users with long-lived tabs.

Changes:

  • Adds chunk-load error detection heuristics and a sessionStorage-based reload-attempt guard.
  • Updates useGlobalErrorCatcher to intercept chunk-related error and unhandledrejection events before the generic toast.
  • Introduces an optional lazyWithRetry helper (in this file) intended for future use.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +118 to +136
/**
* Wrapper opcional para React.lazy que tenta novamente uma vez ao falhar.
* Uso (futuro, em rotas que sofrem com isso):
* const Page = lazy(lazyWithRetry(() => import('./Page')));
*/
export function lazyWithRetry<T>(
importFn: () => Promise<T>,
retries = 1,
): () => Promise<T> {
return async () => {
try {
return await importFn();
} catch (error) {
if (!isChunkLoadError(error) || retries <= 0) throw error;
await new Promise((resolve) => setTimeout(resolve, 800));
return await importFn();
}
};
}
Comment on lines +111 to +113
window.setTimeout(() => {
window.location.reload();
}, CHUNK_RELOAD_DELAY_MS);
Comment on lines +206 to +210
// Carregou com sucesso — limpa o contador de reload após grace period
const clearTimer = window.setTimeout(() => {
clearReloadAttempts();
}, CHUNK_RELOAD_CLEAR_MS);

* Detecta se um erro é causado por chunk JS faltando (stale deploy).
* Cobre os padrões observados em Chromium, Firefox e Safari.
*/
export function isChunkLoadError(error: unknown): boolean {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants