diff --git a/src/renderer/src/lib/lazy-chunk-error-classification.ts b/src/renderer/src/lib/lazy-chunk-error-classification.ts index ba02722202f..90340a56e50 100644 --- a/src/renderer/src/lib/lazy-chunk-error-classification.ts +++ b/src/renderer/src/lib/lazy-chunk-error-classification.ts @@ -15,7 +15,7 @@ export function describeLazyChunkError( return { reloadKey: sanitizeReloadKey(reloadKey), errorName, - errorCategory: classifyErrorCategory(messageClass), + errorCategory: classifyErrorCategory(messageClass, errorName), messageClass } } @@ -30,6 +30,7 @@ function classifyErrorName(error: unknown): string { } switch (error.name) { case 'AggregateError': + case 'ChunkLoadError': case 'DOMException': case 'EvalError': case 'Error': @@ -62,6 +63,7 @@ function classifyErrorMessage(message: string): string { normalized.includes('failed to fetch dynamically imported module') || normalized.includes('error loading dynamically imported module') || normalized.includes('importing a module script failed') || + normalized.includes('failed to load module script') || normalized.includes('loading chunk') || normalized.includes('chunkloaderror') || normalized.includes('networkerror') @@ -71,11 +73,11 @@ function classifyErrorMessage(message: string): string { return 'unknown' } -function classifyErrorCategory(messageClass: string): string { +function classifyErrorCategory(messageClass: string, errorName: string): string { if (messageClass === 'syntax') { return 'syntax' } - if (messageClass === 'fetch') { + if (messageClass === 'fetch' || errorName === 'ChunkLoadError') { return 'fetch' } return 'unknown' diff --git a/src/renderer/src/lib/lazy-with-retry.test.ts b/src/renderer/src/lib/lazy-with-retry.test.ts index 984590e12fb..22a8094ee05 100644 --- a/src/renderer/src/lib/lazy-with-retry.test.ts +++ b/src/renderer/src/lib/lazy-with-retry.test.ts @@ -13,6 +13,15 @@ const chunkFetchError = (): TypeError => new TypeError( 'Failed to fetch dynamically imported module: https://assets.example.test/chunks/panel.js?v=secret /Users/test/orca/panel.js' ) +const chunkLoadErrorByName = (): Error => { + const error = new Error('async route asset request failed') + error.name = 'ChunkLoadError' + return error +} +const failedModuleScriptError = (): TypeError => + new TypeError( + 'Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html".' + ) function markChunkReloadAttemptedInPriorRenderer(): void { window.sessionStorage.setItem(RELOAD_RENDERER_BOOT_KEY, 'previous-renderer-boot') @@ -330,6 +339,34 @@ describe('loadLazyWithRetry', () => { expect(isLazyChunkLoadError(caught)).toBe(true) }) + it.each([ + ['ChunkLoadError name', chunkLoadErrorByName], + ['failed module script message', failedModuleScriptError] + ])('restarts the app for post-reload %s failures', async (_label, createError) => { + const reload = spyOnReload() + const api = stubPreloadRecoveryApi() + markChunkReloadAttemptedInPriorRenderer() + const factory = vi.fn(() => Promise.reject(createError())) + + const loaded = loadLazyWithRetry(factory, { retries: 0, reloadKey: 'route-panel' }) + let settled = false + void loaded.then( + () => { + settled = true + }, + () => { + settled = true + } + ) + await vi.advanceTimersByTimeAsync(5000) + + expect(api.getVersion).toHaveBeenCalledTimes(1) + expect(api.restart).toHaveBeenCalledTimes(1) + expect(window.localStorage.getItem(APP_RESTART_GUARD_KEY)).toBe('1') + expect(reload).not.toHaveBeenCalled() + expect(settled).toBe(false) + }) + it('does not restart the app for non-chunk lazy module failures after renderer reload', async () => { const reload = spyOnReload() const api = stubPreloadRecoveryApi()