fix(qwik-router): make loader response caching opt-in (drop 120s default expires) - #8900
Open
blakeley wants to merge 1 commit into
Open
fix(qwik-router): make loader response caching opt-in (drop 120s default expires)#8900blakeley wants to merge 1 commit into
blakeley wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: d48c19a The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
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 |
@qwik.dev/core
@qwik.dev/router
eslint-plugin-qwik
create-qwik
@qwik.dev/optimizer
@qwik.dev/devtools
commit: |
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.
Fixes #8895
The problem
routeLoaderQrl()defaultsexpiresto 120 seconds:sendLoaderResponse()turns any non-zero__expiresinto a real HTTP caching header:So every
q-loader-{id}.{hash}.jsonresponse shipsCache-Control: private, max-age=120unless the author opted out.Read-after-write staleness
routeAction$/server$that mutates a row.Because the fetch never leaves the browser, this is invisible in server logs and in the Network panel's "from disk cache"-blind reading; it looks like the mutation silently failed.
Repro with header proof and a Playwright demo of the stale render: https://github.com/blakeley/loader-default-120s-cache
The fix
Default
expiresto0, making loader HTTP caching opt-in. The response writer already gates on__expires > 0(quoted above), so0means noCache-Controlheader at all — the browser revalidates every loader fetch and a mutation is visible immediately on the next one. Authors who want caching passexpiresexplicitly, exactly as documented.This restores Qwik 1's effective behavior: loader values were never HTTP-cached there, so apps upgrading to v2 inherit a caching policy they never asked for. Per-loader
expiresremains fully available and unchanged.Notes
0. The options table inpackages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdxlistsexpiresas0(static) — the code and the documentation disagree today; this PR makes the code match. Docs wording is updated to describe caching as opt-in and to warn about whatexpiresimplies.expiresalso drives the client signal's expiry (expires: loader.__expiresis passed to the loader'sAsyncComputedSignal), so the same constant currently also marks loader values stale after 2 minutes. Withpoll: false(the loader default) that only triggers a refetch on next read — and that refetch was itself served from the 120s HTTP cache, so the two windows were coupled. Both become opt-in together.LOADER_FETCH_CACHE_TTL = 5_000inroute-loaders.tsis its own constant for coalescing concurrent/duplicate loader requests; it is not tied toexpiresand this PR leaves it alone.worker-thread.tspre-generates static per-loader.jsonfiles for loaders with__expires === 0. With the new default, plain loaders now get that treatment — which is what the docs already describe forexpires: 0. Worth a maintainer eye if that was intentionally reachable only by opting in.does not cache loader responses unless expires is given); the existing test that asserts an explicitexpiresstill maps toCache-Controlis unchanged and passing. No test asserted the 120s default.@qwik.dev/routerpatch).If the 120s default is intentional
Happy to be told this is deliberate — a default TTL does cut loader traffic meaningfully. If so, I'll pivot this PR to documenting it prominently instead: call it out in the v1 → v2 migration guide (it is a silent behavior change for anyone porting an app that mutates data), fix the options table that currently claims
0, and add the "setexpires: 0on anything a user can mutate" guidance. Your call which way to take it.