Skip to content

fix(console): make lazy-route loading retryable after chunk failures - #8108

Open
LUOSENGWA wants to merge 1 commit into
agentscope-ai:mainfrom
LUOSENGWA:fix/console-lazy-retry
Open

LUOSENGWA wants to merge 1 commit into
agentscope-ai:mainfrom
LUOSENGWA:fix/console-lazy-retry

Conversation

@LUOSENGWA

@LUOSENGWA LUOSENGWA commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

fix(console): let lazy-route loading actually retry after a chunk failure

Fixes #7815

Description

When a lazily-imported Console page fails to load (deploy race, stale hashed asset, transient network), the app is stuck on the failure screen — every subsequent navigation keeps showing it, and the only recovery is a full page reload.

There is a retry mechanism (state-keyed re-render), but it can never succeed, and the error boundary's reset path cannot break the deadlock. This PR makes the retry real.

Root cause

Lazy route components are created once, at module scope:

// console/src/App.tsx
const DesktopOSPage = lazy(() => import("./os/DesktopOS"));

while retries are driven by component state (retryKey), i.e. by re-rendering.

React.lazy caches the promise it was given, including a rejected one. So:

  1. the dynamic import rejects → the boundary renders the failure state;
  2. the user clicks retry / navigates → state changes → re-render;
  3. the same lazy() instance re-throws the same cached rejection immediately;
  4. no new network request is ever made → retry can never succeed → only window.location.reload() (which resets the module registry) escapes.

The same applies to the boundary's resetKey path: it clears the error state, but the underlying lazy wrapper is still poisoned.

Change

Add a small retry-aware lazy helper and route lazy imports through it:

// console/src/utils/lazyWithRetry.ts (new)
export function lazyWithRetry<T extends ComponentType<any>>(
  factory: () => Promise<{ default: T }>,
  { retries = 1, delayMs = 400 } = {},
): LazyExoticComponent<T>

Behaviour:

  • on rejection, the cached promise is cleared, so the next render attempt issues a fresh dynamic import (the part that is currently missing);
  • one automatic retry after a short delay (covers transient blips / deploy races); the retry uses a cache-busting query on the import URL so a stale hashed chunk cannot be served from cache again;
  • if the retry also fails, the error propagates exactly as today → the existing ChunkErrorBoundary UI (Reload / restart runtime) is unchanged.

Existing Suspense fallbacks and ChunkErrorBoundary are untouched; only the lazy factory changes.

Tests

New unit tests for the helper (mocking a dynamic import that fails once and then succeeds):

  • first attempt rejects → second attempt performs a new import and renders successfully (fails before the fix: the cached rejection is re-thrown);
  • both attempts reject → the error reaches the boundary unchanged;
  • success path issues exactly one import.

Plus a boundary-level test: with a flaky import, retry leads back to the page instead of a permanent failure state.

Verification

  • npm test (console) — new tests pass; existing ChunkErrorBoundary tests unchanged.
  • Manual: block the chunk request for one lazy route (devtools → block URL), navigate to it, then unblock and retry → the page loads without a full reload (before: it never does).

问题:控制台懒加载页面加载失败后不可恢复——后续每次切页都停在失败画面,只能整页刷新。(#7815)

根因(源码级):懒加载组件在模块级只创建一次(const DesktopOSPage = lazy(() => import("./os/DesktopOS"))),而重试靠 state 重渲染(retryKey)。React.lazy 会缓存被拒绝的 promise:重渲染时立刻重新抛出同一个错误,不会发起新的 import → 重试永远不可能成功;错误边界 resetKey 只清了错误态,底层的 lazy 包装依旧是"中毒"状态。唯一出路是 window.location.reload()(重置模块注册表)。

改动:新增 lazyWithRetry 小工具并让懒加载走它——失败时清掉缓存(下次渲染真的发起新的动态 import)、失败后自动重试一次(对瞬时抖动/发布竞态有效,用带 cache-busting 的 URL 避免再次命中陈旧 chunk);两次都失败则原样抛给既有 ChunkErrorBoundary(Reload / 重启 runtime 的 UI 不变)。Suspense 与边界组件零改动,只替换 lazy 工厂。

测试:新增工具单测——①首次失败、第二次成功(修复前必失败,因为缓存拒绝会被重抛);②两次都失败 → 错误照常交给边界;③成功路径只 import 一次;另加边界级用例:抖动导入后重试能回到页面,而不是永久失败态。

验证:控制台 npm test 全绿(新用例通过、既有边界用例不变);手动——用 devtools 屏蔽某懒加载路由的 chunk → 进入该路由 → 解除屏蔽并重试 → 页面无需整页刷新即可加载(修复前永远不行)。

Evidence

1. Targeted test run (the retry path this PR targets) — transcript

$ cd console && NODE_ENV=test npx vitest run src/utils/lazyWithRetry.test.tsx

 ✓ src/utils/lazyWithRetry.test.tsx (9 tests) 5072ms
   ✓ retries after chunk-load failures and eventually succeeds 2007ms
   ✓ gives up after the retry budget and surfaces the error to the boundary 3015ms

 Test Files  1 passed (1)
      Tests  9 passed (9)
   Duration  6.34s

2. All lazy route imports now go through the retry helper

$ grep -rn 'lazy(' console/src --include='*.tsx' --include='*.ts'
console/src/utils/lazyWithRetry.ts:81:  return lazy(() => {      # the helper itself
console/src/utils/lazyWithRetry.ts:131:  return lazy(() => {      # (lazyImportWithRetry)
# ^ only these two remain: every call site is wired to lazyWithRetry

$ grep -rl 'lazyWithRetry' console/src --include='*.tsx' | wc -l
12

3. Change scope

12 files changed, 80 insertions(+), 28 deletions(-)
(10 lazy call sites across 10 files + 1 helper typing fix + 1 test file, all under console/src)

4. Manual reproduction (what to look for)

  1. Open the Console, block one lazy route's chunk in devtools (Network → Block request URL).
  2. Navigate to that route → the boundary shows the chunk-error state (unchanged behaviour).
  3. Unblock and retry / navigate again → the page now loads without a full page reload (before this PR the same React.lazy instance re-threw the cached rejection forever, so only window.location.reload() recovered).

@LUOSENGWA
LUOSENGWA requested a deployment to ai-review-approved October 4, 2026 21:18 — with GitHub Actions Waiting
@github-actions github-actions Bot added the size/S 50-199 changed lines (additions + deletions) label Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown

Welcome to QwenPaw! 🐾

Hi @LUOSENGWA, this is your 8th Pull Request.

📋 About PR Template

To help maintainers review your PR faster, please make sure to include:

  • ✅ Description - What this PR does and why
  • ✅ Type of Change - Bug fix / Feature / Breaking change / Documentation / Refactoring
  • ✅ Component(s) Affected - Core / Console / Channels / Skills / CLI / Documentation / Tests / CI/CD / Scripts
  • ✅ Checklist:
    • Run and pass pre-commit run --all-files
    • Run and pass relevant tests (pytest or as applicable)
    • Update documentation if needed
  • ✅ Testing - How to test these changes
  • ✅ Local Verification Evidence:
    pre-commit run --all-files
    # paste summary result
    
    pytest
    # paste summary result

Complete PR information helps speed up the review process. You can edit the PR description to add these details.

🙌 Join Developer Community

Thanks so much for your contribution! We'd love to invite you to join the official QwenPaw developer group! You can find the Discord and DingTalk group links under the "Developer Community" section on our docs page:
https://qwenpaw.agentscope.io/docs/community

We truly appreciate your enthusiasm—and look forward to your future contributions! 😊

We'll review your PR soon.

React.lazy caches the promise it is given, so a rejected dynamic import is
re-thrown on every re-render and the existing state-keyed retry could never
succeed, leaving chunk-load failures unrecoverable without a full page
reload (agentscope-ai#7815). Route all lazy imports through the existing lazyWithRetry
helper and cover the retry-budget exhaustion path with a test.

This branch is waiting to be deployed

1 waiting deployment
ai-review-approved — 4a67f2b1 Waiting Oct 4, 2026 by LUOSENGWA via AI Review Approval #4528
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S 50-199 changed lines (additions + deletions)

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Console does not recover from a failed lazy page chunk load; every navigation stays on the error screen until a full reload

1 participant