cloudflare / cloudflare/cloudflare-os
Stale blueprint request can overwrite a newer route and target actions at the wrong blueprint
- Dominant language
- TypeScript
- Stars
- 9.9k
- Forks
- 1.2k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 107
Description
## Suggested severity
High (S2): an ordinary client-side route change can leave the page showing one blueprint while its actions operate on the blueprint ID in the current URL.
## Summary
`BlueprintLandingPage` does not associate an asynchronous `getBlueprint(id)` result with the route that started it. TanStack Router reuses the component when only the `$id` parameter changes, so an older request can settle after a newer request and replace the current blueprint state.
A controlled-promise test reproduces this deterministically on unmodified commit `8b08672050e6957987a87b5f9cab2801ba89b42c`.
## Deterministic reproduction on unpatched `main`
1. Render `/blueprint/blueprint-a` and keep `getBlueprint("blueprint-a")` pending.
2. Re-render the same route component for `/blueprint/blueprint-b`.
3. Resolve B with metadata titled `CURRENT BLUEPRINT`.
4. Resolve the older A request with metadata titled `STALE BLUEPRINT`.
5. Inspect the current route and rendered page.
### Actual result
The route remains `/blueprint/blueprint-b`, but A's title and metadata overwrite the page. A previously loaded blueprint can also appear during the first render of the next route. If B rejects after A has loaded, A remains visible instead of B's error.
### Expected result
Only the request associated with the current `id` and RPC stub should update the page. The previous blueprint should stop rendering as soon as the route changes.
## Impact
This mismatch is more than cosmetic. Event handlers use the current route `id`, while labels, confirmation text, binding metadata, and download filenames use the `blueprint` object in state. For example, a delete confirmation can name A while `deleteBlueprint(id)` targets B, and a download can use A's filename while requesting B's archive.
## Root cause
The metadata-loading effect has no cleanup or generation guard. Every `.then()`, `.catch()`, and `.finally()` callback can update shared state after its route is stale. Loaded state is also tied only to the reused component, rather than to the `(id, rpcStub)` load that produced it.
## Proposed fix
- Cancel state updates from an effect when its `id` or `rpcStub` is superseded.
- Track which `(id, rpcStub)` produced the loaded blueprint, preventing old content from rendering during the route-change commit before the next effect runs.
- Clear the previous blueprint and error when starting the next load.
- Cover out-of-order responses, the first render after navigation, not-found responses, and rejected requests.
Contributor guide
Research direction
Start at BlueprintLandingPage and inspect the metadata-loading effect and the state associated with the current id and rpcStub. Use the controlled-promise reproduction described in the issue to exercise out-of-order responses, navigation, not-found results, and rejection. Done means stale requests cannot update the page, old content disappears on navigation, and the relevant tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100