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

Closed
VaguelySerious wants to merge 4 commits into
stablefrom
peter/fix-stream-cancel-reconnect
Closed

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

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Apr 17, 2026

Copy link
Copy Markdown
Member

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

changeset-botBot commented Apr 17, 2026

Copy link
Copy Markdown

🦋 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
NameType
@workflow/world-vercelPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production900167968
✅ 🪟 Windows880088
Total9881671056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro8017
✅ example8107
✅ express8107
✅ fastify8107
✅ hono8107
✅ nextjs-turbopack8602
✅ nextjs-webpack8602
✅ nitro8107
✅ nuxt8107
✅ sveltekit8107
✅ vite8107
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8800

📋 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
VaguelySeriousforce-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dcCompareApril 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 freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Closed
VaguelySerious wants to merge 4 commits into
stablefrom
peter/fix-stream-cancel-reconnect
Closed

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

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Apr 17, 2026

Copy link
Copy Markdown
Member

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

changeset-botBot commented Apr 17, 2026

Copy link
Copy Markdown

🦋 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
NameType
@workflow/world-vercelPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production900167968
✅ 🪟 Windows880088
Total9881671056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro8017
✅ example8107
✅ express8107
✅ fastify8107
✅ hono8107
✅ nextjs-turbopack8602
✅ nextjs-webpack8602
✅ nitro8107
✅ nuxt8107
✅ sveltekit8107
✅ vite8107
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8800

📋 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
VaguelySeriousforce-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dcCompareApril 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 freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Closed
VaguelySerious wants to merge 4 commits into
stablefrom
peter/fix-stream-cancel-reconnect
Closed

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

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Apr 17, 2026

Copy link
Copy Markdown
Member

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

changeset-botBot commented Apr 17, 2026

Copy link
Copy Markdown

🦋 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
NameType
@workflow/world-vercelPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production900167968
✅ 🪟 Windows880088
Total9881671056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro8017
✅ example8107
✅ express8107
✅ fastify8107
✅ hono8107
✅ nextjs-turbopack8602
✅ nextjs-webpack8602
✅ nitro8107
✅ nuxt8107
✅ sveltekit8107
✅ vite8107
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8800

📋 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
VaguelySeriousforce-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dcCompareApril 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 freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Closed
VaguelySerious wants to merge 4 commits into
stablefrom
peter/fix-stream-cancel-reconnect
Closed

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

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Apr 17, 2026

Copy link
Copy Markdown
Member

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

changeset-botBot commented Apr 17, 2026

Copy link
Copy Markdown

🦋 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
NameType
@workflow/world-vercelPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production900167968
✅ 🪟 Windows880088
Total9881671056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro8017
✅ example8107
✅ express8107
✅ fastify8107
✅ hono8107
✅ nextjs-turbopack8602
✅ nextjs-webpack8602
✅ nitro8107
✅ nuxt8107
✅ sveltekit8107
✅ vite8107
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8800

📋 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
VaguelySeriousforce-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dcCompareApril 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 freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Closed
VaguelySerious wants to merge 4 commits into
stablefrom
peter/fix-stream-cancel-reconnect
Closed

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

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Apr 17, 2026

Copy link
Copy Markdown
Member

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

changeset-botBot commented Apr 17, 2026

Copy link
Copy Markdown

🦋 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
NameType
@workflow/world-vercelPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production900167968
✅ 🪟 Windows880088
Total9881671056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro8017
✅ example8107
✅ express8107
✅ fastify8107
✅ hono8107
✅ nextjs-turbopack8602
✅ nextjs-webpack8602
✅ nitro8107
✅ nuxt8107
✅ sveltekit8107
✅ vite8107
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8800

📋 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
VaguelySeriousforce-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dcCompareApril 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 freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Closed
VaguelySerious wants to merge 4 commits into
stablefrom
peter/fix-stream-cancel-reconnect
Closed

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

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Apr 17, 2026

Copy link
Copy Markdown
Member

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

changeset-botBot commented Apr 17, 2026

Copy link
Copy Markdown

🦋 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
NameType
@workflow/world-vercelPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production900167968
✅ 🪟 Windows880088
Total9881671056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro8017
✅ example8107
✅ express8107
✅ fastify8107
✅ hono8107
✅ nextjs-turbopack8602
✅ nextjs-webpack8602
✅ nitro8107
✅ nuxt8107
✅ sveltekit8107
✅ vite8107
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8800

📋 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
VaguelySeriousforce-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dcCompareApril 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 freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Closed
VaguelySerious wants to merge 4 commits into
stablefrom
peter/fix-stream-cancel-reconnect
Closed

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

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Apr 17, 2026

Copy link
Copy Markdown
Member

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

changeset-botBot commented Apr 17, 2026

Copy link
Copy Markdown

🦋 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
NameType
@workflow/world-vercelPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production900167968
✅ 🪟 Windows880088
Total9881671056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro8017
✅ example8107
✅ express8107
✅ fastify8107
✅ hono8107
✅ nextjs-turbopack8602
✅ nextjs-webpack8602
✅ nitro8107
✅ nuxt8107
✅ sveltekit8107
✅ vite8107
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8800

📋 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
VaguelySeriousforce-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dcCompareApril 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 freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Closed
VaguelySerious wants to merge 4 commits into
stablefrom
peter/fix-stream-cancel-reconnect
Closed

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

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Apr 17, 2026

Copy link
Copy Markdown
Member

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

changeset-botBot commented Apr 17, 2026

Copy link
Copy Markdown

🦋 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
NameType
@workflow/world-vercelPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production900167968
✅ 🪟 Windows880088
Total9881671056

❌ Failed Tests

▲ Vercel Production (1 failed)

astro (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
❌ astro8017
✅ example8107
✅ express8107
✅ fastify8107
✅ hono8107
✅ nextjs-turbopack8602
✅ nextjs-webpack8602
✅ nitro8107
✅ nuxt8107
✅ sveltekit8107
✅ vite8107
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8800

📋 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
VaguelySeriousforce-pushed the peter/fix-stream-cancel-reconnect branch from 6e2acd6 to 85cb3dcCompareApril 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 freeto 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

@VaguelySerious