Skip to content

[world-vercel] Propagate cancel from getReadable() to upstream fetch#1801

Draft
VaguelySerious wants to merge 4 commits intostablefrom
peter/fix-stream-cancel-reconnect
Draft

[world-vercel] Propagate cancel from getReadable() to upstream fetch#1801
VaguelySerious wants to merge 4 commits intostablefrom
peter/fix-stream-cancel-reconnect

Conversation

@VaguelySerious
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious commented Apr 17, 2026

Summary

Fixes a regression from #1790 / #1742: when a consumer cancels the ReadableStream returned by run.getReadable() (e.g. an HTTP client disconnects from an API route that pipes the stream out), the pull loop in readFromStream kept running and could trigger further reconnects in the background. The in-flight upstream fetch was never aborted, so the endpoint kept fetching long after the client had gone away.

Root cause

Two things combined:

  1. No cancelled flag — pull had no way to know the consumer asked to stop.
  2. No AbortSignal on the upstream fetch() — even if we noticed, there was no way to unblock a pending request.

The cancel handler only called reader.cancel() on whichever reader was captured at that moment. If pull was mid-reconnect (reader = await connect()), cancel cancelled the stale reader; the new fetch then connected and kept streaming.

Fix

  • Track a cancelled flag that pull checks before and after each reader.read() and before each connect().
  • Plumb an AbortController.signal into fetch, aborted from cancel, so the in-flight request (including a pending reconnect) unblocks.
  • connect() failures triggered by abort are swallowed silently when cancelled.

Tests

Three new tests under `readFromStream reconnection > consumer cancel`:

  • `aborts the in-flight upstream fetch via AbortSignal` — fails on `stable` without the fix; asserts `fetch` was called with an `AbortSignal` and that the signal is aborted after `stream.cancel()`.
  • `does not reconnect after the consumer cancels mid-timeout` — documents that no further fetch is initiated after cancel.
  • `cancels the active reader when cancel is called during a read` — documents that cancel unblocks a pending `reader.read()`.

All 70 world-vercel tests pass.

Note

The same bug exists on `main` (the original #1742 implementation that `main` carries). Will open a separate PR for that once this lands.

Test plan

  • `pnpm vitest run` — all world-vercel tests pass
  • Regression test fails on pre-fix code (`stable` HEAD)
  • Typecheck clean
  • CI + workflow-server auto e2e

🤖 Generated with Claude Code

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 17, 2026

🦋 Changeset detected

Latest commit: 4a98ba5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@workflow/world-vercel Patch
@workflow/cli Patch
@workflow/core Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/ai Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 17, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 900 1 67 968
✅ 🪟 Windows 88 0 0 88
Total 988 1 67 1056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
❌ astro 80 1 7
✅ example 81 0 7
✅ express 81 0 7
✅ fastify 81 0 7
✅ hono 81 0 7
✅ nextjs-turbopack 86 0 2
✅ nextjs-webpack 86 0 2
✅ nitro 81 0 7
✅ nuxt 81 0 7
✅ sveltekit 81 0 7
✅ vite 81 0 7
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 88 0 0

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: failure
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

When a consumer cancels the ReadableStream returned by `readFromStream`
(e.g. an HTTP client hanging up on an endpoint that pipes
`run.getReadable()`), the pull loop could continue running and even
trigger a fresh reconnect via `connect()` — the new fetch was never
tied to the cancellation, so the request kept running in the
background.

Fix:
- Track a `cancelled` flag that `pull` checks before and after each
  read and before each reconnect.
- Plumb an `AbortController.signal` into `fetch` so the in-flight
  upstream request (including a pending reconnect) is aborted when
  the consumer cancels.

Regression tests cover the abort-signal contract, the mid-timeout
reconnect race, and cancel-during-read.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
@VaguelySerious VaguelySerious force-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dc Compare April 17, 2026 19:16
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
VaguelySerious added a commit that referenced this pull request Apr 17, 2026
When a consumer cancels the ReadableStream returned by streams.get
(e.g. an HTTP client hanging up on an endpoint that pipes
`run.getReadable()`), the pull loop could continue running and even
trigger a fresh reconnect via `connect()` — the new fetch was never
tied to the cancellation, so the request kept running in the
background.

Fix:
- Track a `cancelled` flag that `pull` checks before and after each
  read and before each reconnect.
- Plumb an `AbortController.signal` into `fetch` so the in-flight
  upstream request (including a pending reconnect) is aborted when
  the consumer cancels.

Regression tests cover the abort-signal contract, the mid-timeout
reconnect race, and cancel-during-read.

Port of #1801 (targeting `stable`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
VaguelySerious added a commit to vercel/workflow-examples that referenced this pull request Apr 17, 2026
- Point workflow tarballs at peter/fix-stream-cancel-reconnect branch preview
- Log request.signal.aborted in the stream route

The companion workflow PR (vercel/workflow#1801) adds cancel propagation
to world-vercel readFromStream. World-vercel's cancel handler logs
'Cancelling stream' when hit; combined with the route.ts log, we can
trace whether the cancel makes it all the way through.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant