fix(kit): restore sync_store after with_request_store resolves in WebContainer - #16822
fix(kit): restore sync_store after with_request_store resolves in WebContainer#16822okxint wants to merge 2 commits into
Conversation
…Container In environments without AsyncLocalStorage (StackBlitz / WebContainer), sync_store is the only request-context carrier. The previous implementation used try/finally, which fires when fn() *returns* the Promise — not when it *resolves* — leaving sync_store permanently set to the query-scoped store (is_in_remote_query: true). Any code that ran after `await query()` would then observe stale state and throw "Cannot access event.url in a query". The fix captures `previous = sync_store` before the call and restores it via Promise.finally() for async fns, or immediately for sync fns. A guard (`sync_store === store`) prevents the .finally() from clobbering a concurrently-set store. Fixes sveltejs#16818 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Install the latest version of pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/c45301df1056dd247231d2e2251555913a43b101Open in Note This PR is from a fork. A maintainer must approve approve each commit before it can be built and installed. |
🦋 Changeset detectedLatest commit: c45301d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| const previous = sync_store; | ||
| sync_store = store; | ||
|
|
||
| const result = als ? als.run(store, fn) : fn(); |
There was a problem hiding this comment.
| const result = als ? als.run(store, fn) : fn(); | |
| let result; | |
| try { | |
| result = als ? als.run(store, fn) : fn(); | |
| } catch (error) { | |
| // If fn() throws synchronously, reset sync_store before rethrowing so a | |
| // stale store isn't left behind (which would leak into subsequent | |
| // synchronous reads / other requests). | |
| if (!IN_WEBCONTAINER) { | |
| sync_store = null; | |
| } else if (sync_store === store) { | |
| sync_store = previous; | |
| } | |
| throw error; | |
| } |
with_request_store fails to reset sync_store when fn() throws synchronously, leaking a stale request store across subsequent synchronous reads and requests.
dummdidumm
left a comment
There was a problem hiding this comment.
We can't do this because it means that you cannot asynchronously access getRequestEvent in web containers anymore (e.g. call it after an await expression inside your query function)
|
Thanks for the review! I want to make sure I understand the concern correctly before reworking the fix. My mental model of the fix: the const myQuery = query(async () => {
const data = await db.fetch() // ← suspend here
const e = getRequestEvent() // ← resume here — sync_store still = queryStore
return { data }
})The execution sequence I expect:
If that model is wrong — e.g. there's a case where step 4/5 actually sees The bug I was targeting is user code after |
What
In WebContainer (StackBlitz),
AsyncLocalStorageis unavailable, sosync_storeis the sole request-context carrier. When a remote query (await query()) completes, the stale query-scoped store — which hasis_in_remote_query: true— was left insync_store. Any user code running after theawaitwould read that store and trigger the throwing getter onevent.url, producing:Fixes #16818.
Root cause
with_request_storeused atry/finallypattern:The
finallyfires whenfn()returns its Promise — not when the Promise resolves. In WebContainer, the block is skipped, leavingsync_storeset tostore(the query store withis_in_remote_query: true) for the entire lifetime of the request.Fix
Capture
previous = sync_storebefore the call and restore it viaPromise.finally()for async functions, or immediately for synchronous ones. A guard (sync_store === store) prevents the.finally()from clobbering a concurrently-set store.Tests
Added
src/exports/internal/server/event.spec.jswith 4 unit tests that mockIN_WEBCONTAINER = trueand disableAsyncLocalStorageto exercise the exact code path:.finally()from clobbering a newer storeAll tests pass (
vitest run --config kit.vitest.config.js).