Skip to content

fix(core): start forwarded writable key lookup on first write (backport of #4175) - #4176

Merged
pranaygp merged 2 commits into
stablefrom
fix/3935-forwarded-writable-key-unhandled-rejection-stable
Sep 15, 2026
Merged

pranaygp merged 2 commits into
stablefrom
fix/3935-forwarded-writable-key-unhandled-rejection-stable

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Description

Backport of #4175 to stable. Fixes #3935 on the 4.8.x line.

Opened by hand rather than left to the backport bot: stable's getForwardedWritableEncryptionKey takes two parameters and returns a CryptoKey, so the main commit does not cherry-pick cleanly, and the exposure here is larger than on main (below).

When a step's arguments or a run's return value contain a writable forwarded from another run, the reviver called getForwardedWritableEncryptionKey() immediately and stored the promise it returned. The only consumer of that promise is the serialize transform, which awaits it on the first chunk written — so on a writable nobody writes to, nothing ever observes it. When the lookup failed first, Node saw an unhandled rejection and killed the process, taking unrelated work in the same invocation with it.

The failing path is world.runs.get(ownerRunId) — GET /v2/runs/:id?remoteRefBehavior=resolve, the request that timed out after ~72s in the report. Only the Vercel World implements getEncryptionKeyForRun, so this is Vercel-only.

The fix is a memoized thunk, which is the resolver form EncryptionKeyParam on this branch already documents:

/**
 * ... When a resolver function is passed, the underlying fetch isn't even
 * initiated until the first chunk is processed, which avoids unobserved
 * background lookups for empty or never-read streams.
 */
export type EncryptionKeyParam =
  | CryptoKey
  | undefined
  | Promise<CryptoKey | undefined>
  | (() => Promise<CryptoKey | undefined>);

The writable reviver was passing the Promise arm where it wanted the resolver arm. Both boundaries now use the resolver: getExternalRevivers (the client path, reached from await run.returnValue) and getStepRevivers (forwarded writables in step arguments). The lookup runs at most once, starts only when a write needs the key, and a failure rejects that stream instead of escaping.

Important

stable is more exposed than main. The 4.8.x line has no encryptionPublicKey fast path in getForwardedWritableEncryptionKey, so every cross-run forwarded writable falls through to runs.get — not just descriptors minted by older deployments. Any app on 4.8.x that forwards a writable across start() is exposed on every hydration of such a payload. The reporter hit this on workflow 4.8.5.

Scope and history

How did you test your changes?

Four regression tests in packages/core/src/serialization.test.ts, adapted to this branch's setWorld() harness and placed next to the existing forwarded-writable tests:

cd packages/core && WORKFLOW_TARGET_WORLD=local pnpm vitest run src/serialization.test.ts
Test Guards
client hydrates a return value it never writes to no unhandledRejection, and no request at all
step hydrates arguments it never writes to same, on the other reviver boundary
surfaces a failed lookup on the writer that needed the key the failure is still delivered on writer.closed, and runs.get runs exactly once
resolves the owner key at most once across many writes the thunk memoizes; no per-write request storm

Each mocks runs.get to reject with the WorkflowWorldError { code: 'TIMEOUT' } shape world-vercel's makeRequest raises.

Verified the tests fail without the fix. Reverting only the two call sites (keeping the tests) fails both unhandled-rejection tests with AssertionError: expected [ Array(1) ] to deeply equal [], the array holding the timeout message. The other two pass either way by design — they pin behaviour the fix must preserve.

Green on this branch:

  • packages/core unit: 866 passed, 3 expected-fail, 3 skipped (48 files)
  • pnpm --filter @workflow/core typecheck: clean
  • biome check on both changed files: no new findings (2 pre-existing noUnusedVariables errors and 5 warnings, identical to stable)

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
    • patch, matching the main changeset
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

🤖 Generated with Claude Code

Backport of the `main` fix for #3935, hand-ported because the 4.8.x
`getForwardedWritableEncryptionKey` takes two parameters and returns a
`CryptoKey`, so the commit does not cherry-pick cleanly.

Hydrating a payload that carries a writable forwarded from another run
called `getForwardedWritableEncryptionKey()` inside the reviver and stored
the promise it returned. Nothing awaits that promise until the first chunk
is written, so when the lookup failed first — e.g. the `runs.get` fallback
for a descriptor from an older deployment timing out — the rejection had no
handler and Node exited the process. A caller that never touched the stream
lost its whole invocation.

Wrap the lookup in a memoized thunk instead. This is the resolver form
`EncryptionKeyParam` already documents ("avoids unobserved background
lookups for empty or never-read streams"), so the lookup starts on the
first write and a failure rejects that stream. Both reviver boundaries are
covered: the client one (`getExternalRevivers`) and the step one
(`getStepRevivers`).

This line is more exposed than `main`: it has no `encryptionPublicKey` fast
path, so every cross-run forwarded writable takes the failing `runs.get`
path.

Fixes #3935

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 15, 2026 08:44
@pranaygp
pranaygp requested review from a team and ijjk as code owners September 15, 2026 08:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
example-nextjs-workflow-turbopack Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
example-nextjs-workflow-webpack Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
example-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-astro-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-express-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-fastify-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-hono-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-nestjs-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-nitro-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-nuxt-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-python-workflow Error Error v0 Sep 15, 2026 9:10am UTC
workbench-sveltekit-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-tanstack-start-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workbench-vite-workflow Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workflow-swc-playground Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workflow-tarballs Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
workflow-web Ready Ready Preview, v0 Sep 15, 2026 9:10am UTC
1 Skipped Deployment
Project Deployment Actions Updated
workflow-docs Skipped Skipped v0 Sep 15, 2026 9:10am UTC

@changeset-bot

changeset-bot Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 08886b8

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

This PR includes changesets to release 16 packages
Name Type
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing 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

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

❌ Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1099 0 78 1177
✅ 💻 Local Development 1198 0 86 1284
✅ 📦 Local Production 1198 0 86 1284
✅ 🐘 Local Postgres 1198 0 86 1284
✅ 🪟 Windows 107 0 0 107
❌ 🌍 Community Worlds 82 106 9 197
✅ 📋 Other 606 0 36 642
Total 5488 106 381 5975

❌ Failed Tests

🌍 Community Worlds (106 failed)

redis (21 failed):

  • hookWorkflow | wrun_01M2J57ZRHV1QGRWQ6A732Q2TN
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01M2J587R51YYQGA02VEX11MMQ
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_01M2J58HXZ3GZ4K0KNMM6K4W3N
  • sleepingWorkflow | wrun_01M2J59FDANW4BDQDDR8HWJNEH
  • outputStreamWorkflow negative startIndex (reads from end)
  • outputStreamWorkflow - getTailIndex and getStreamChunks getTailIndex returns correct index after stream completes
  • outputStreamWorkflow - getTailIndex and getStreamChunks getTailIndex returns -1 before any chunks are written
  • outputStreamWorkflow - getTailIndex and getStreamChunks getStreamChunks returns same content as reading the stream
  • error handling serialization failures step-argument serialization failure is catchable in workflow code
  • error handling serialization failures uncaught step-argument serialization failure fails the run as USER_ERROR without redelivery retries
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01M2J5H80N8XSNQEGFWNPKSWWQ
  • hookGetConflictWorkflow - awaiting hook.getConflict() registers hook without payload | wrun_01M2J5HK83B540RJNF1JYQPW78
  • hookGetConflictThenStepParallelWorkflow - hook.getConflict() continuation step runs alongside other steps | wrun_01M2J5HV58WHBEY3RPJRP9GFGN
  • hookGetConflictWorkflow - hook.getConflict() resolves with the conflicting run when token is already registered | wrun_01M2J5J988BVAS40W9VQ2QAN46
  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_01M2J5K3F7WG1J3Y6R1T4P2D70
  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_01M2J5K75A7H9DN8QWHXJAGFRK
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_01M2J5KCP29M7000185939E2FT
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_01M2J5KJ3GHMXZXK5QQ4NP7W43
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_01M2J5KTNNAE78JDVJAX6H4XMM
  • pages router sleepingWorkflow via pages router
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01M2J5S1RVM6HGY1336CV20G1A

turso (85 failed):

  • addTenWorkflow | wrun_01M2J575020YB60ERK3RMCRSWD
  • addTenWorkflow | wrun_01M2J575020YB60ERK3RMCRSWD
  • deploymentId: 'latest' is a no-op in non-Vercel worlds
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01M2J598R4F0BTKRM78STDNS25
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01M2J57B9E0VV263Z7M7JQX8DB
  • promiseRaceWorkflow | wrun_01M2J57ERNYH735H4ERD9WW5AP
  • promiseAnyWorkflow | wrun_01M2J57H4P01G8J2VH6CCVNA32
  • importedStepOnlyWorkflow | wrun_01M2J59Q5JBYA396EY7WZYBHCY
  • readableStreamWorkflow | wrun_01M2J57KEH2Y52NNXDA6MJEWZM
  • hookWorkflow | wrun_01M2J57ZRHV1QGRWQ6A732Q2TN
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01M2J587R51YYQGA02VEX11MMQ
  • webhookWorkflow | wrun_01M2J58D5BJ1Q1B8FSEX8XK0ES
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_01M2J58HXZ3GZ4K0KNMM6K4W3N
  • sleepingWorkflow | wrun_01M2J59FDANW4BDQDDR8HWJNEH
  • parallelSleepWorkflow | wrun_01M2J59Y71HV5RJB5A7ERAM46H
  • sleepWinsRaceWorkflow | wrun_01M2J5A1SCBJ82GHH3ZA5S89N9
  • stepWinsRaceWorkflow | wrun_01M2J5A5A8QPW7DH2QBQQC3VHM
  • nullByteWorkflow | wrun_01M2J5A8V8P22B7NP9ATNENXDW
  • workflowAndStepMetadataWorkflow | wrun_01M2J5ABXZYPEZNEN53APXQB98
  • outputStreamWorkflow no startIndex (reads all chunks)
  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow negative startIndex (reads from end)
  • outputStreamWorkflow - getTailIndex and getStreamChunks getTailIndex returns correct index after stream completes
  • outputStreamWorkflow - getTailIndex and getStreamChunks getTailIndex returns -1 before any chunks are written
  • outputStreamWorkflow - getTailIndex and getStreamChunks getStreamChunks returns same content as reading the stream
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions | wrun_01M2J5CM5PHF7SRA83M354AGX3
  • writableForwardedFromWorkflowWorkflow | wrun_01M2J5D0X44EA3ATQ5R5V259P2
  • writableForwardedFromStepWorkflow | wrun_01M2J5D4VD44ABSDASV4ERF7NG
  • fetchWorkflow | wrun_01M2J5D7HH36T601NQJ6JWVQSC
  • promiseRaceStressTestWorkflow | wrun_01M2J5DB2FSFW15JXK8GJ62SSH
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling serialization failures step-argument serialization failure is catchable in workflow code
  • error handling serialization failures uncaught step-argument serialization failure fails the run as USER_ERROR without redelivery retries
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01M2J5GX1F002H9BN9M1BPWPBM
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01M2J5H80N8XSNQEGFWNPKSWWQ
  • hookGetConflictWorkflow - awaiting hook.getConflict() registers hook without payload | wrun_01M2J5HK83B540RJNF1JYQPW78
  • 'hookGetConflictWithPriorStepWorkflow' - hook.getConflict() does not block step execution | wrun_01M2J5HNRD6GDQJF975FSQVDKA
  • 'hookGetConflictWithParallelStepWorkfl…' - hook.getConflict() does not block step execution | wrun_01M2J5HREAS1KMAB48QWSS7846
  • hookGetConflictThenStepParallelWorkflow - hook.getConflict() continuation step runs alongside other steps | wrun_01M2J5HV58WHBEY3RPJRP9GFGN
  • hookGetConflictWorkflow - hook.getConflict() resolves with the conflicting run when token is already registered | wrun_01M2J5J988BVAS40W9VQ2QAN46
  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_01M2J5K3F7WG1J3Y6R1T4P2D70
  • hookAdoptOwnerResultWorkflow - duplicate adopts the owner result via conflict.returnValue | wrun_01M2J5K75A7H9DN8QWHXJAGFRK
  • hookSignalOwnerWorkflow - duplicate forwards its payload to the owner via resumeHook | wrun_01M2J5KCP29M7000185939E2FT
  • hookSupersedeOwnerWorkflow - duplicate cancels the owner and claims the released token | wrun_01M2J5KJ3GHMXZXK5QQ4NP7W43
  • resume-or-start route pattern - resumeHook retried after start() reaches the new run | wrun_01M2J5KTNNAE78JDVJAX6H4XMM
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01M2J5M1P3FNVQ958PVA776MH9
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01M2J5MFZZ9DJJF3C0Y4VAT9T3
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01M2J5MR1R1542QEBERSJ212WM
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01M2J5MX3YGSG5E0S5VNCV4AN7
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01M2J5MZEEB20TVFMN3W9MDWVN
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01M2J5NE3AR1BCR5WD0GWZY1FH
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01M2J5NKEBJ1H11CG6GSGFM99Q
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01M2J5NRKQSC37XBQ4K4YRFBQ7
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01M2J5NXPTVCNR2A4MQ15J7GZK
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01M2J5P2R1S9JPT3ZH3JEVHX0F
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01M2J5P90DYFC5Q28VEWAFKQT2
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01M2J5PFEH00NF731SY54RJ33X
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01M2J5PV8TBCVRR51YE2QF2TDN
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01M2J5Q393RYF5YGXTPSBG30ES
  • cancelRun - cancelling a running workflow | wrun_01M2J5Q9P3ZWWG9HZJM6YYYRK0
  • cancelRun via CLI - cancelling a running workflow | wrun_01M2J5QDSZW9BTT7KMWSZXZ9R5
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01M2J5QM69HGHWPGQH4KTD0Q0T
  • hookWithSleepFinalStepWorkflow - step only on final payload | wrun_01M2J5QZWD0FJN0QG95W4HGFKF
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01M2J5RBSQ50J90BQFACA625MP
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01M2J5RPHPTJHN607HR879HE6Z
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01M2J5RWXXM5C9G4102B65APE4
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01M2J5RZD0PXSZDE7KDVM2RP4K
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01M2J5S1RVM6HGY1336CV20G1A

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 99 0 8
✅ example 99 0 8
✅ express 99 0 8
✅ fastify 99 0 8
✅ hono 99 0 8
✅ nextjs-turbopack 104 0 3
✅ nextjs-webpack 104 0 3
✅ nitro 99 0 8
✅ nuxt 99 0 8
✅ sveltekit 99 0 8
✅ vite 99 0 8
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 101 0 6
✅ express-stable 101 0 6
✅ fastify-stable 101 0 6
✅ hono-stable 101 0 6
✅ nextjs-turbopack-canary 88 0 19
✅ nextjs-turbopack-stable 107 0 0
✅ nextjs-webpack-canary 88 0 19
✅ nextjs-webpack-stable 107 0 0
✅ nitro-stable 101 0 6
✅ nuxt-stable 101 0 6
✅ sveltekit-stable 101 0 6
✅ vite-stable 101 0 6
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 101 0 6
✅ express-stable 101 0 6
✅ fastify-stable 101 0 6
✅ hono-stable 101 0 6
✅ nextjs-turbopack-canary 88 0 19
✅ nextjs-turbopack-stable 107 0 0
✅ nextjs-webpack-canary 88 0 19
✅ nextjs-webpack-stable 107 0 0
✅ nitro-stable 101 0 6
✅ nuxt-stable 101 0 6
✅ sveltekit-stable 101 0 6
✅ vite-stable 101 0 6
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 101 0 6
✅ express-stable 101 0 6
✅ fastify-stable 101 0 6
✅ hono-stable 101 0 6
✅ nextjs-turbopack-canary 88 0 19
✅ nextjs-turbopack-stable 107 0 0
✅ nextjs-webpack-canary 88 0 19
✅ nextjs-webpack-stable 107 0 0
✅ nitro-stable 101 0 6
✅ nuxt-stable 101 0 6
✅ sveltekit-stable 101 0 6
✅ vite-stable 101 0 6
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 107 0 0
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 4 0 3
✅ redis-dev 4 0 3
❌ redis 67 21 0
✅ turso-dev 4 0 3
❌ turso 3 85 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 101 0 6
✅ e2e-local-dev-tanstack-start-stable 101 0 6
✅ e2e-local-postgres-nest-stable 101 0 6
✅ e2e-local-postgres-tanstack-start-stable 101 0 6
✅ e2e-local-prod-nest-stable 101 0 6
✅ e2e-local-prod-tanstack-start-stable 101 0 6

📋 View full workflow run

Copy link
Copy Markdown
Contributor Author

CI triage — the two red jobs are pre-existing on stable

Both also fail on this branch's base commit e3862bc13, the last completed stable run:

Job On e3862bc13 (base) Here
E2E Community World (Turso) ❌ fail ❌ fail
Vercel – workbench-python-workflow deployment failure deployment failure

That baseline run was red overall, with E2E Vercel Prod Tests (nextjs-turbopack, sveltekit, example), E2E Community World (Redis) and E2E Required Check failing too — none of which this diff touches. Neither red job here exercises cross-run forwarded writables.

The checks that this change can affect are green: DCO, Unit Tests (ubuntu-latest), CI Scripts Tests, No Test Overrides, Node.js Module Build Errors Test.

My token can't rerun --failed, so flagging for a maintainer if a clean run is wanted.

The backport asserted only that `writer.closed` rejects. Assert the same
`cause: { code: 'TIMEOUT' }` the `main` test does, so both branches pin
that the world error survives to the writer rather than just that some
error does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
@pranaygp
pranaygp requested a lite review from Copilot September 15, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel
vercel Bot temporarily deployed to Preview – workflow-docs September 15, 2026 09:07 Inactive

Copy link
Copy Markdown
Contributor Author

CI status — every gate passes; the 4 reds are chronically broken lanes on stable

DCO                        ✅
Unit Tests (ubuntu-latest) ✅
Unit Tests (windows-latest)✅
E2E Required Check         ✅

The four red checks fail on stable regardless of this PR. Across the last five completed stable runs:

Lane e3862bc (base) 9595a5a ee29d15 a158f81 d86d752
E2E Turso fail fail fail fail fail
E2E Redis fail fail fail cancelled fail
E2E MongoDB cancelled cancelled cancelled cancelled cancelled
Vercel – workbench-python-workflow fail — — — —

Turso is 5/5 red, Redis 4/5, MongoDB never completes, and the python workbench deployment fails on the base commit too. Retrying reproduces them, so I haven't burned CI on it — these need owners, not a re-run. None of them exercise cross-run forwarded writables.

For contrast, the main companion #4175 is now fully green (180/180) after two retriggers cleared genuinely transient Windows and Vercel-upload flakes.

One change since the first push

08886b86 tightens the backport's test to assert the same thing the main test does:

-        await expect(writer.closed).rejects.toThrow();
+        await expect(writer.closed).rejects.toMatchObject({
+          cause: { code: 'TIMEOUT' },
+        });

I verified the cause chain this branch actually produces before tightening it, so the assertion pins real behaviour rather than a guess:

WorkflowRuntimeError: Failed to serialize stream chunk…
  └▶ WorkflowWorldError: GET /v2/runs/wrun_owner?remoteRefBehavior=resolve timed out after 71779ms  (code=TIMEOUT)

Both branches now pin that the world error survives to the writer, not merely that some error does. 127 tests pass in serialization.test.ts; no new Biome findings (the 2 noUnusedVariables errors are pre-existing on stable).

@pranaygp
pranaygp merged commit e1f712b into stable Sep 15, 2026
97 of 101 checks passed
@pranaygp
pranaygp deleted the fix/3935-forwarded-writable-key-unhandled-rejection-stable branch September 15, 2026 19:42
@github-actions github-actions Bot mentioned this pull request Sep 15, 2026

This branch had an error being deployed

1 failed, 16 active, and 1 inactive deployments
Preview – workflow-swc-playground — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – example-nextjs-workflow-webpack — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-tanstack-start-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – example-nextjs-workflow-turbopack — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-sveltekit-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-fastify-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-nuxt-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-astro-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – example-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-express-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-vite-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-hono-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-nitro-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-nestjs-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workflow-tarballs — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workflow-web — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workbench-python-workflow — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
Preview – workflow-docs — 08886b86 Deployed Sep 15, 2026 by vercel[bot]
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.

3 participants