cloudflare / cloudflare/cloudflare-os

Stale blueprint request can overwrite a newer route and target actions at the wrong blueprint

Open
#146 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.