fix(editor): Auto-reload editor on stale Vite chunk preload error (#31747)

Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
n8n-cat-bot[bot]
2026-06-11 13:05:01 +00:00
committed by GitHub
co-authored by n8n-cat-bot[bot] Claude Opus 4.7
parent 5fa988104b
commit 2f3ebb620c
5 changed files with 100 additions and 0 deletions
@@ -38,6 +38,16 @@ describe('beforeSend', () => {
expect(beforeSend(event, hint)).toBeNull();
});
it.each([
'Failed to fetch dynamically imported module: https://example.com/assets/RunDataSearch-abc.js',
'error loading dynamically imported module: https://example.com/assets/RunDataSearch-abc.js',
'Importing a module script failed.',
])('should return null for stale-chunk preload TypeError: %s', (message) => {
const event = createErrorEvent();
const hint = { originalException: new TypeError(message) };
expect(beforeSend(event, hint)).toBeNull();
});
it('should return event when originalException does not match any ignoredErrors', () => {
const event = createErrorEvent();
const hint = { originalException: new Error('Some other error') };
@@ -14,6 +14,12 @@ const ignoredErrors = [
{ instanceof: RangeError, message: /Invalid change range \d+ to \d+/ },
{ instanceof: RangeError, message: /Selection points outside of document$/ },
{ instanceof: Error, message: /ResizeObserver/ },
// Stale-chunk preload errors after a deploy — the page auto-reloads to recover.
// Known blind spot: a broken deploy (chunk missing for everyone) produces the same
// message and is also suppressed; rely on CI / smoke checks for that signal.
{ instanceof: TypeError, message: /Failed to fetch dynamically imported module/ },
{ instanceof: TypeError, message: /error loading dynamically imported module/ },
{ instanceof: TypeError, message: /Importing a module script failed/ },
] as const;
type SentryConfig = {
@@ -0,0 +1,66 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { registerVitePreloadErrorHandler } from '@/app/plugins/vitePreloadError';
const STORAGE_KEY = 'n8n:vite-preload-reloaded-at';
function dispatchPreloadError() {
const event = new Event('vite:preloadError', { cancelable: true });
window.dispatchEvent(event);
return event;
}
describe('registerVitePreloadErrorHandler', () => {
let reloadSpy: ReturnType<typeof vi.fn>;
let originalLocation: Location;
let unregister: () => void;
beforeEach(() => {
sessionStorage.clear();
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
originalLocation = window.location;
reloadSpy = vi.fn();
Object.defineProperty(window, 'location', {
configurable: true,
value: { ...originalLocation, reload: reloadSpy },
});
unregister = registerVitePreloadErrorHandler();
});
afterEach(() => {
unregister();
vi.useRealTimers();
Object.defineProperty(window, 'location', {
configurable: true,
value: originalLocation,
});
sessionStorage.clear();
});
it('reloads and stores a timestamp on the first preload error', () => {
const event = dispatchPreloadError();
expect(reloadSpy).toHaveBeenCalledTimes(1);
expect(event.defaultPrevented).toBe(true);
expect(Number(sessionStorage.getItem(STORAGE_KEY))).toBe(Date.now());
});
it('does not reload on a second preload error within the throttle window', () => {
dispatchPreloadError();
vi.advanceTimersByTime(5_000);
const event = dispatchPreloadError();
expect(reloadSpy).toHaveBeenCalledTimes(1);
expect(event.defaultPrevented).toBe(false);
});
it('reloads again after the throttle window has elapsed', () => {
dispatchPreloadError();
vi.advanceTimersByTime(10_001);
dispatchPreloadError();
expect(reloadSpy).toHaveBeenCalledTimes(2);
});
});
@@ -0,0 +1,15 @@
const VITE_PRELOAD_RELOAD_TS = 'n8n:vite-preload-reloaded-at';
const RELOAD_THROTTLE_MS = 10_000;
export function registerVitePreloadErrorHandler(): () => void {
const handler = (event: Event) => {
const last = Number(sessionStorage.getItem(VITE_PRELOAD_RELOAD_TS) ?? 0);
if (Date.now() - last < RELOAD_THROTTLE_MS) return;
sessionStorage.setItem(VITE_PRELOAD_RELOAD_TS, String(Date.now()));
event.preventDefault();
window.location.reload();
};
window.addEventListener('vite:preloadError', handler);
return () => window.removeEventListener('vite:preloadError', handler);
}
+3
View File
@@ -27,11 +27,14 @@ import { GlobalDirectivesPlugin } from '@/app/plugins/directives';
import { createPinia, PiniaVuePlugin } from 'pinia';
import { ChartJSPlugin } from '@/app/plugins/chartjs';
import { SentryPlugin } from '@/app/plugins/sentry';
import { registerVitePreloadErrorHandler } from '@/app/plugins/vitePreloadError';
import { registerModuleRoutes } from '@/app/moduleInitializer/moduleInitializer';
import { installRenderTracker } from '@/app/dev/render-tracker';
import type { VueScanOptions } from 'z-vue-scan';
registerVitePreloadErrorHandler();
const pinia = createPinia();
const app = createApp(App);