Note on how this is filed
I have a fix, tests, and a pushed branch ready, but pull request creation against this repo is currently blocked at the platform level for external forks (POST /repos/mistralai/client-ts/pulls and the GraphQL createPullRequest mutation both return a hard permission denial for a fork-based PR, independent of branch/content -- confirmed with gh and with a raw curl call). The last external fork PR that actually got created was #200 in March 2026, closed and landed via a maintainer cherry-pick into #230 crediting the original author -- so I'm following that same path and filing this as an issue with the fix attached, per CONTRIBUTING.md.
Branch (open for a maintainer to pull/cherry-pick directly): https://github.com/shoemoney/client-ts/tree/fix/workflow-stream-error-frames
Diff: https://github.com/mistralai/client-ts/compare/main...shoemoney:client-ts:fix/workflow-stream-error-frames.diff
The bug
client-python already guards against this. client-ts never got the equivalent.
The four workflow-stream endpoints (workflow execution stream, workflow events stream, and the two log-stream endpoints) send an HTTP 200 and then, if something goes wrong server-side mid-stream, emit an in-band event: error SSE frame instead of just closing the connection. The generated response model documents this itself:
// src/models/operations/streamv1workflowsexecutionsexecutionidstreamget.ts:29
/**
* SSE event name. `error` indicates the stream failed after HTTP 200.
*/
event?: string | undefined;
EventStream (src/lib/event-streams.ts) parses and yields every SSE frame uniformly, with no special case for event: error. The documented usage pattern in this repo has no error check either:
// docs/sdks/workflowsevents/README.md
for await (const event of result) {
console.log(event);
}
So a consumer following the SDK's own docs gets the error frame handed to them as an ordinary item and has no reason to suspect the stream failed. client-python closed this gap in mistralai/client-python#596 (WorkflowStreamErrorHook, raising StreamDisconnectedError when a frame scanner detects an error frame). The fix below ports that hook to client-ts.
Reachable from workflows.executions.stream(), workflows.events.getStreamEvents(), and the two log-stream methods.
The fix
New hook src/hooks/stream_error_hook.ts, registered in src/hooks/registration.ts alongside the existing three (CustomUserAgentHook, DeprecationWarningHook, TracingHook) -- same shape as those, nothing else touched.
For the four guarded operation IDs (matching client-python's STREAM_OPERATIONS_WITH_ERROR_EVENT exactly: get_stream_events_v1_workflows_events_stream_get, stream_v1_workflows_executions__execution_id__stream_get, stream_deployment_logs, stream_workflow_execution_logs), the hook's afterSuccess wraps response.body in a TransformStream that buffers bytes and scans for SSE frame boundaries -- a frame scanner rather than a naive per-chunk check, because the frame (including the blank-line boundary itself) can be split across arbitrary HTTP chunk boundaries. When it finds a complete event: error frame it throws WorkflowStreamDisconnectedError (mirroring client-python's StreamDisconnectedError: same reason/error fields, same reason union of read_error / stream_error / internal_error) instead of forwarding the frame, so the consumer's for await loop throws mid-iteration instead of receiving the error payload as data. Every other operation and every non-SSE response passes through completely untouched.
No suitable existing public error type covered this (everything in src/models/errors/ is generated and shaped around an HTTP error response, not a mid-stream failure after 200). WorkflowStreamDisconnectedError follows the existing hand-written precedent for this exact situation: src/extra/realtime/errors.ts's RealtimeTranscriptionException/RealtimeTranscriptionWSError, a plain Error subclass colocated with its subsystem.
Nothing generated is touched: src/lib/event-streams.ts, src/funcs/, src/models/ are all untouched. The change lives entirely in a new hand-written hook file plus the one hand-written registration file, both explicitly marked "free to be modified" in their own header comments.
What I ran
From client-ts/:
npm install (root deps)
npx tsgo --noEmit -- clean
npm run lint (oxlint, --max-warnings=0 --deny-warnings) -- 0 warnings, 0 errors, 333 files
cd tests && npm install && npx vitest run -- 186 passed (179 pre-existing + 7 new), 0 failed
RED/GREEN check on the new tests: deleted src/hooks/stream_error_hook.ts and reverted registration.ts -- the new test file fails hard (module not found). Restored both files -- back to 186/186 green.
New tests (tests/hooks/stream_error_hook.test.ts):
- a stream ending in an
event: error frame throws through EventStream, and the frame(s) before it are still delivered first
- a normal stream with no error frame is byte-for-byte unchanged, and yields every event through
EventStream with no throw
- an operation ID outside the guarded set is untouched (same
Response instance returned) even when its body looks like an error frame
- an error frame split across a chunk boundary is still detected -- both a split inside the blank-line boundary itself and a split mid-field
- a non-
text/event-stream response for a guarded operation ID is untouched
Out of scope
client-python also has a WorkflowEncodingHook with no client-ts equivalent. Noting it as an observation only -- not addressed here.
Note on how this is filed
I have a fix, tests, and a pushed branch ready, but pull request creation against this repo is currently blocked at the platform level for external forks (
POST /repos/mistralai/client-ts/pullsand the GraphQLcreatePullRequestmutation both return a hard permission denial for a fork-based PR, independent of branch/content -- confirmed withghand with a rawcurlcall). The last external fork PR that actually got created was #200 in March 2026, closed and landed via a maintainer cherry-pick into #230 crediting the original author -- so I'm following that same path and filing this as an issue with the fix attached, per CONTRIBUTING.md.Branch (open for a maintainer to pull/cherry-pick directly): https://github.com/shoemoney/client-ts/tree/fix/workflow-stream-error-frames
Diff: https://github.com/mistralai/client-ts/compare/main...shoemoney:client-ts:fix/workflow-stream-error-frames.diff
The bug
client-pythonalready guards against this.client-tsnever got the equivalent.The four workflow-stream endpoints (workflow execution stream, workflow events stream, and the two log-stream endpoints) send an HTTP 200 and then, if something goes wrong server-side mid-stream, emit an in-band
event: errorSSE frame instead of just closing the connection. The generated response model documents this itself:EventStream(src/lib/event-streams.ts) parses and yields every SSE frame uniformly, with no special case forevent: error. The documented usage pattern in this repo has no error check either:So a consumer following the SDK's own docs gets the error frame handed to them as an ordinary item and has no reason to suspect the stream failed.
client-pythonclosed this gap in mistralai/client-python#596 (WorkflowStreamErrorHook, raisingStreamDisconnectedErrorwhen a frame scanner detects an error frame). The fix below ports that hook toclient-ts.Reachable from
workflows.executions.stream(),workflows.events.getStreamEvents(), and the two log-stream methods.The fix
New hook
src/hooks/stream_error_hook.ts, registered insrc/hooks/registration.tsalongside the existing three (CustomUserAgentHook,DeprecationWarningHook,TracingHook) -- same shape as those, nothing else touched.For the four guarded operation IDs (matching client-python's
STREAM_OPERATIONS_WITH_ERROR_EVENTexactly:get_stream_events_v1_workflows_events_stream_get,stream_v1_workflows_executions__execution_id__stream_get,stream_deployment_logs,stream_workflow_execution_logs), the hook'safterSuccesswrapsresponse.bodyin aTransformStreamthat buffers bytes and scans for SSE frame boundaries -- a frame scanner rather than a naive per-chunk check, because the frame (including the blank-line boundary itself) can be split across arbitrary HTTP chunk boundaries. When it finds a completeevent: errorframe it throwsWorkflowStreamDisconnectedError(mirroring client-python'sStreamDisconnectedError: samereason/errorfields, samereasonunion ofread_error/stream_error/internal_error) instead of forwarding the frame, so the consumer'sfor awaitloop throws mid-iteration instead of receiving the error payload as data. Every other operation and every non-SSE response passes through completely untouched.No suitable existing public error type covered this (everything in
src/models/errors/is generated and shaped around an HTTP error response, not a mid-stream failure after 200).WorkflowStreamDisconnectedErrorfollows the existing hand-written precedent for this exact situation:src/extra/realtime/errors.ts'sRealtimeTranscriptionException/RealtimeTranscriptionWSError, a plainErrorsubclass colocated with its subsystem.Nothing generated is touched:
src/lib/event-streams.ts,src/funcs/,src/models/are all untouched. The change lives entirely in a new hand-written hook file plus the one hand-written registration file, both explicitly marked "free to be modified" in their own header comments.What I ran
From
client-ts/:npm install(root deps)npx tsgo --noEmit-- cleannpm run lint(oxlint,--max-warnings=0 --deny-warnings) -- 0 warnings, 0 errors, 333 filescd tests && npm install && npx vitest run-- 186 passed (179 pre-existing + 7 new), 0 failedRED/GREEN check on the new tests: deleted
src/hooks/stream_error_hook.tsand revertedregistration.ts-- the new test file fails hard (module not found). Restored both files -- back to 186/186 green.New tests (
tests/hooks/stream_error_hook.test.ts):event: errorframe throws throughEventStream, and the frame(s) before it are still delivered firstEventStreamwith no throwResponseinstance returned) even when its body looks like an error frametext/event-streamresponse for a guarded operation ID is untouchedOut of scope
client-pythonalso has aWorkflowEncodingHookwith noclient-tsequivalent. Noting it as an observation only -- not addressed here.