Skip to content

fix(viewer): malformed URL hash no longer bricks the whole client#186

Open
ivanmkc wants to merge 1 commit into
masterfrom
fix/hash-decode-crash
Open

fix(viewer): malformed URL hash no longer bricks the whole client#186
ivanmkc wants to merge 1 commit into
masterfrom
fix/hash-decode-crash

Conversation

@ivanmkc

@ivanmkc ivanmkc commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Symptom

/w/demo/ with a malformed URL-hash fragment loads a broken page: status stuck on connecting…, an empty board list, a blank main pane, and (on the read-only demo) the Console toggle still showing. It survives a hard refresh. A valid deeplink like #ops%2Forder-fulfillment works fine.

Root cause

readHash() runs decodeURIComponent(location.hash…) at module top level (viewer.ts). decodeURIComponent throws URIError: URI malformed on a bad %-escape (#%, #%zz, #foo%2, …). The throw aborts the entire client boot before connect() is ever called — so the page is frozen in its static index.html shell:

indicator cause
status stuck connecting… initial HTML value; connect() never ran
empty board list / blank main snapshot never requested
Console toggle still present read-only cleanup (which removes it) never ran
survives hard-refresh the bad fragment stays in the URL

A bare URL (empty hash) and a valid hash both decode fine — which is exactly why the deeplink worked while the affected URL didn't.

Reproduced in Chromium against both current source and the live deployment; confirmed the deployed bundle is byte-identical to source (not a stale-deploy issue).

Fix

New pure helper safeDecodeHash() (packages/viewer/src/client/hash.ts) wraps the decode in try/catch; an undecodable hash degrades to "" (treated as no deep-link), so boot continues and the app auto-selects the first board / shows the gallery — the behavior the user expected ("autoselect something … not completely fail"). readHash() now calls it (covers both the top-level read and the hashchange handler).

Tests / verification

  • New unit test packages/viewer/test/hash.test.ts (TDD: written failing first) — normal/empty/plain hashes + malformed %-escapes never throw, degrade to "".
  • Full viewer suite green (623 passed).
  • E2E (Playwright) before/after on /w/demo/#%, #%zz, #foo%2: beforeconnecting…, 0 boards, blank, URI malformed throw; after● live, 15 boards, first board auto-selected, no throw. Bare URL and valid deeplink still work.

Rollout

Client-side fix — the deployed Cloud Run viewer must be redeployed for it to reach users; a local dev viewer picks it up on rebuild.

A malformed %-escape in the URL hash (e.g. "#%", "#%zz", "#foo%2") made
decodeURIComponent throw URIError. readHash() runs that at module top level,
so an undecodable hash aborted the ENTIRE client boot before connect() — the
page froze in its static shell: status stuck "connecting…", an empty board
list, a blank main pane, and (on the read-only demo) the console-toggle never
removed. It survived a hard refresh because the bad fragment stays in the URL.

Decode defensively via a new safeDecodeHash() helper: an undecodable hash
degrades to "" (treated as no deep-link), so boot continues and the app
auto-selects the first board / shows the gallery, exactly as a bare URL does.

A valid hash and a bare URL were always fine — which is why a deeplink worked
while the affected URL didn't.
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