Relationships
#4445 swamp-lib: workflowRunStream drops a refusal and ends as if nothing went wrong
Opened by skunk-ape · 11/5/2026
Summary
SwampClient.workflowRunStream() in @swamp-club/swamp-lib drops an error frame when the consumer is already waiting for the next event. A refused run ends the stream as if nothing went wrong: no events, no exception. A caller can read a refused run as a clean finish.
Reproduction
Call workflowRunStream with a token that has no run grant on the workflow:
for await (const e of client.workflowRunStream({ workflowIdOrName: "gated" })) { /* never entered */ }
// the loop exits normallySeen on abeba3f6a with swamp-lib 0.2.0 from packages/client, under Node 26, against a local serve in token mode, using a read-only user.
| Call | Result |
|---|---|
| Raw WebSocket | error frame: unauthorized, "Access denied: user:bob does not have 'run' on workflow:gated" |
workflowRunStream |
no events, and nothing thrown |
workflowRun |
rejected with that unauthorized error |
Serve refused correctly and no run started. The loss is only in the client.
Cause
In packages/client/client.ts, AsyncIterableQueue.error() stores the error, then settles a waiting next() with { done: true }. The stored error is only raised by a later next(), and for await never makes one after done. An error that arrives before the consumer starts waiting is raised correctly, so this depends on timing.
modelMethodRunStream uses the same queue. I read that from the code and haven't tested it.
Expected
A waiting next() rejects with the error, so the stream throws SwampClientError.
Impact
Any integration using the stream helpers treats refusals, and possibly other errors, as success with no output. A test Slack app showed nothing at all when a person without run tried to start a workflow.
Related: #4438. workflowRun and workflowRunStream currently have opposite failures, so neither is safe alone for a workflow that can both suspend and be refused.
Open
No activity in this phase yet.
Sign in to post a ripple.