Skip to content

fix(scenes): guard scene URL handling against malformed input - #70254

Merged
pauldambra merged 2 commits into
masterfrom
posthog-code/guard-scene-url-handling
Jul 12, 2026
Merged

fix(scenes): guard scene URL handling against malformed input#70254
pauldambra merged 2 commits into
masterfrom
posthog-code/guard-scene-url-handling

Conversation

@posthog

@posthog posthog Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Problem

Malformed URLs crash scene rendering because sceneLogic decodes and routes them without guarding the input. Two separate first-observation error tracking crashes trace back to the same weak spot:

  • PersonScene's paramsToProps calls decodeURIComponent(rawUrlId) directly, which throws URIError: URI malformed on a stray % (think a distinct id like 50%off).
  • A URL containing whitespace throws during kea-router route matching (argument must not contain whitespace) inside the route-mapping arrows that build redirect URLs and dispatch openScene/loadScene.

Both run synchronously inside kea-router's route dispatch, so nothing catches the throw and the page fails to render. Blast radius is small (both are new, low-occurrence issues), but they are real crashes rather than cosmetic glitches.

Why: raised from an inbox report about the two crashes above. The fix hardens the scene-loading path so an oddly-encoded URL degrades gracefully instead of taking down the whole app.

Changes

  • PersonScene.tsx: decode the URL id with the existing tryDecodeURIComponent helper from lib/utils/url, which falls back to the raw id on URIError instead of throwing.
  • sceneLogic.tsx: wrap the urlToAction redirect and scene-dispatch handlers in a guardRoute helper. If building a redirect URL or dispatching a scene throws, it captures the exception and falls back to Error404 so the app renders a 404 instead of crashing.

Note

The catch-all /* handler already loads Error404, so it is left unwrapped.

No frontend visual changes.

How did you test this code?

Automated tests I (Claude) actually ran:

  • Added PersonScene.test.tsx: a parameterized test over scene.paramsToProps covering a valid encoding (50%25off50%off), a malformed % that must fall back to the raw id (50%off50%off), and whitespace passthrough (foo bar). This locks in the decode fallback — it fails if someone reverts to a bare decodeURIComponent. No existing test exercised PersonScene's paramsToProps.
  • Ran the new test plus the existing sceneLogic.test.ts/.tsx suites (24 tests) — all pass.
  • Type-checked both changed files (clean) and ran the frontend formatter.

I did not manually exercise the malformed URLs in a running browser.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Authored by Claude (Claude Code) acting on a PostHog inbox report. I traced both crashes to the scene-routing path in sceneLogic.tsx and the direct decodeURIComponent in PersonScene.tsx.

Decisions along the way:

  • Reused the existing tryDecodeURIComponent helper instead of adding a new try/catch, since it already does exactly the raw-id fallback.
  • Guarded the route handlers rather than the deeper kea-router internals: per the report the throw surfaces synchronously through the route-mapping arrows, so wrapping those catches it regardless of the exact library frame, and routing to Error404 matches how sceneLogic already handles malformed scene params elsewhere (setScene's logic-builder catch).
  • Skipped a dedicated test for the sceneLogic guard: guardRoute is a local closure and triggering the real throw needs kea-router internals, which would be a brittle, expensive test. Invoked the /writing-tests skill to make that call.

Created with PostHog Code from an inbox report

Malformed URLs crash scene rendering because sceneLogic decodes and routes
them without guarding the input.

- PersonScene's paramsToProps called decodeURIComponent on the raw URL id,
  which throws URIError on a stray `%` (e.g. a distinct id like `50%off`).
  Use the existing tryDecodeURIComponent helper so it falls back to the raw
  id instead of crashing the scene.
- sceneLogic's urlToAction handlers build redirect URLs and dispatch scene
  loads synchronously while kea-router matches the route. A malformed or
  whitespace-containing URL could throw there with nothing to catch it,
  failing the whole page. Wrap the handlers so a throw is captured and the
  route falls back to Error404.

Generated-By: PostHog Code
Task-Id: 6e5d3078-ffbd-4039-becb-607e900c2d42
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Bundle size — 🔺 +261 B (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 68.09 MiB · 🔺 +261 B (+0.0%)

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.21 MiB · 22 files no change ███░░░░░░░ 28.2% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.10 MiB · 2,974 files 🔺 +247 B (+0.0%) █████████░ 87.5% of 9.25 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
668 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
278.2 KiB ../node_modules/.pnpm/posthog-js@1.399.2/node_modules/posthog-js/dist/rrweb.js
266.9 KiB ../node_modules/.pnpm/@posthog+icons@0.37.4_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
234.9 KiB src/taxonomy/core-filter-definitions-by-group.json
221.5 KiB ../node_modules/.pnpm/posthog-js@1.399.2/node_modules/posthog-js/dist/module.js
164.0 KiB src/queries/validators.js
154.2 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.9 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

⚠️ Dist folder size — 🔺 +8.7 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1283.94 MiB · 🔺 +8.7 KiB (+0.0%)

Playwright — all passed

All tests passed.

View test results →

@pauldambra
pauldambra marked this pull request as ready for review July 11, 2026 22:26
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 11, 2026 22:26

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

Copy link
Copy Markdown

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: 69655d0c92

ℹ️ 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 thread frontend/src/scenes/sceneLogic.tsx
@trunk-io

trunk-io Bot commented Jul 11, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

kea-router runs decodeURI(pathname) while matching routes, before any scene
loads. A stray `%` that isn't a valid escape (e.g. a direct visit or popstate
to `/person/50%off`) makes decodeURI throw URIError synchronously inside the
router, crashing the whole app before the route handler or PersonScene ever
run. The urlToAction handler guard added earlier sits downstream of this
throw, so it can't catch it.

Add `ensureRoutablePathname` and apply it in `transformPathInActions`, which
kea-router runs on the pathname before decodeURI on every navigation (initial
load, push/replace, popstate). Escaping a lone `%` keeps the path decodable so
routing continues to the matched scene or falls through to 404 instead of
throwing.

Also tighten the guardRoute catch to use the non-optional `payload.method`
directly.

Generated-By: PostHog Code
Task-Id: 6e5d3078-ffbd-4039-becb-607e900c2d42
@pauldambra pauldambra added the stamphog Request AI approval (no full review) label Jul 11, 2026
@stamphog

stamphog Bot commented Jul 11, 2026

Copy link
Copy Markdown

stamphog does not review bot-authored PRs — removing the stamphog label. This change needs a human reviewer.

@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Jul 11, 2026
@pauldambra
pauldambra merged commit e857e8c into master Jul 12, 2026
295 of 327 checks passed
@pauldambra
pauldambra deleted the posthog-code/guard-scene-url-handling branch July 12, 2026 11:08
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-12 11:28 UTC Run
prod-us ✅ Deployed 2026-07-12 11:38 UTC Run
prod-eu ✅ Deployed 2026-07-12 11:43 UTC Run

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.

1 participant