fix: stop resolveHref/gotoHref throwing on bracketed paths - #327
Open
FrameAutomata wants to merge 1 commit into
Open
fix: stop resolveHref/gotoHref throwing on bracketed paths#327FrameAutomata wants to merge 1 commit into
FrameAutomata wants to merge 1 commit into
Conversation
resolve() from $app/paths takes a route id and populates its [param] segments by dereferencing a params argument. Both helpers handed it a concrete pathname and no params, so any path containing brackets threw `TypeError: Cannot read properties of undefined (reading '<name>')`. safeLocalPath lets brackets through, so /login?returnTo=%2Fa%5Bb%5Dc reaches it. In login/+page.svelte the throw lands after the token is stored, and the form's own catch renders it as a credentials error -- the user ends up logged in, still on the login page, reading a TypeError. auth/callback has the same shape. These hrefs are already concrete, so they need the base path, not route resolution. That is also what goto() documents wanting for root-relative URLs. gotoHref now delegates to resolveHref rather than repeating it, and finish-setup routes its SSO returnTo through gotoHref instead of open-coding a bare goto() that drops the base path. Also widens the passthrough guard, which required '//' and so let scheme-only URIs through to resolve(). That is not merely a missing base prefix: resolve_route does route.slice(1) unconditionally, so it ate the first character -- 'mailto:x@y.z' came back as '/ailto:x@y.z'. Latent today; nothing in src/ passes one, but button.svelte and otel-setup-steps.svelte forward hrefs they do not control. The $app/paths test stub returned its argument unchanged, which is why no test ever saw this. It is replaced by SvelteKit's real client module so the new tests fail on the unfixed code. Closes #325
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #325.
resolve()from$app/pathstakes a route id and populates its[param]segments by dereferencing a params argument. Both helpers handed it a concrete pathname and no params, so any path containing brackets threw. Reproduced against the installed kit 2.49.2:safeLocalPathonly requires a single leading/, so/login?returnTo=%2Fa%5Bb%5Dcreaches it. Inlogin/+page.sveltethe throw lands afterauthState.setToken(...), and the form's owncatchrenders it as a credentials error — the user ends up logged in, still on the login page, readingCannot read properties of undefined (reading 'b').auth/callbackhas the same shape.The fix
These hrefs are already concrete, so they need the base path, not route resolution — which is also what
goto()documents wanting ("if you've setpaths.baseand the URL is root-relative, you need to prepend the base path").gotoHrefnow delegates toresolveHrefinstead of repeating its body; that duplication is why one bug lived in two places. Theas '/'cast that suppressed the type error is gone.The scheme-only guard is worse than "missing base prefix"
The old guard required
//, somailto:/tel:fell through toresolve(). That doesn't just prepend a base —get_route_segmentsdoesroute.slice(1)unconditionally, assuming a leading/:It eats the first character of any href without a leading slash. Latent — nothing in
src/passes one today — butbutton.svelte:70andotel-setup-steps.svelte:108forward hrefs they don't control. The guard is now/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.Why no test caught this
src/test/mocks/app-paths.tsstubbedresolveaspath => path— identity. Any test written against it passes pre- and post-fix and pins nothing. The alias now points at SvelteKit's real client module, so the new tests fail on the unfixed code for the right reason:Two approaches that don't work, so they aren't re-tried: the
sveltekit()plugin resolves$app/pathsto the server variant and breaks the threeflame-graphcomponent tests, and a bare deep import is blocked by kit'sexportsmap.$app/navigationstill needs a stub (goto()requires a mounted router); it throws rather than no-ops so an unmocked navigation fails loudly.Performance
Incidental but measured, since
resolveHrefruns per<a>per render. Old path allocated ~4 arrays and ran a per-segment regexreplace; new path is two concatenations.resolve())Two calls worth a reviewer's opinion
1. The vitest alias reaches into kit's
src/.node_modules/@sveltejs/kit/src/runtime/app/paths/client.jsis not in the package'sexportsmap, so a kit reshuffle breaks it. I kept it anyway: with abase-only stub, pre-fix code fails everyresolveHrefassertion with "resolve is not a function" rather than isolating the bracket defect, and the failure mode on upgrade is loud (unresolved import), not silent. Reasonable to overrule — reverting costs the diagnostic quality above and nothing else, since no module under test callsresolve()today.2.
finish-setup/+page.sveltewill conflict with #324. Onmainthat line isgoto(returnTo)— no crash, but it drops the base path and is one of #316's 10 lint errors. #324 fixes it by hand-inlininggoto(base + returnTo)with its own suppression and a near-verbatim copy of this PR's comment. Routing it throughgotoHrefinstead means this branch doesn't ship the general mechanism while a special case sits open-coded next door. The conflict is one line, take-mine, and it drops project-wide eslint errors from 10 to 9.Known and deliberately not fixed
gotoHrefdouble-prefixesbasewhen a caller passes an already-resolved href —+error.svelte:18andpost-mortems-tab.svelte:156passresolve()output throughcreateRowClickHandler;url-params.ts:113and+layout.svelte:227passwindow.location.pathname. Pre-existing and unchanged: the oldgotoHrefcalledresolve(), which prependsbaseidentically. Fixing it is a policy decision across ~8 sites — the base-path job fix: let npm run lint join CI #324 scoped out.dashboards/+page.svelte:950—resolve(page.url.pathname as '/'), same cast, re-appliesbaseto a pathname that already has it. Brackets unreachable (static route). Untouched because fix: let npm run lint join CI #324 already edits that file. Note this is a different site from thesetServerScopeone fix: let npm run lint join CI #324 fixes.resolve()silently collapsed/issues//footo/issues/foo; the new code preserves the doubled slash. Nothing produces one (addStickyParamsToHrefnormalizes vianew URL), so no collapse was added back.All other non-literal
resolve()call sites were checked: status-page slugs are backend-validated against^[a-z0-9][a-z0-9-]{1,58}[a-z0-9]$, and the rest either wrap the value inencodeURIComponent(which percent-encodes brackets) or interpolate DB ids/hashes.Verification
npm run test29 passed (5 files) ·npm run check0 errors, 12 warnings unchanged ·npm run buildok · eslint/prettier clean on changed files · project-wide eslint 9 errors, down from main's 10.Note
npm run lintstill fails on this base — those are #316's pre-existing prettier/eslint failures, fixed by #324.🤖 Generated with Claude Code