anomalyco / anomalyco/opencode
SDK SSE abort leaks an unhandled reader.cancel() rejection under Bun
@nexxeln is already working on this.
Since Sep 8, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Description
The generated SSE client in @opencode-ai/sdk calls reader.cancel() from an AbortSignal handler without observing the returned promise:
const abortHandler = () => {
try {
reader.cancel()
} catch {
// noop
}
}
ReadableStreamDefaultReader.cancel() returns a promise. The try/catch only handles a synchronous throw. When reader.read() is pending and aborting the fetch has already errored the body stream, that promise can reject with AbortError. Under Bun, the resulting unhandled rejection can terminate a long-running host process.
The same pattern is present in both generated clients:
packages/sdk/js/src/gen/core/serverSentEvents.gen.tspackages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts
Current dev still contains the unhandled call in the v2 client: https://github.com/anomalyco/opencode/blob/dev/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts#L116-L123
Expected behavior: aborting an SDK SSE subscription should not emit an unhandled rejection.
The minimal fix is to observe the cancellation promise in both generated clients (or fix the generator/template so regeneration preserves it):
void reader.cancel().catch(() => {})
Plugins
N/A — using @opencode-ai/sdk directly.
OpenCode version
Reproduced with @opencode-ai/sdk 1.18.28. The unsafe call is also present in 1.18.29/current dev.
Steps to reproduce
- Start a real SSE HTTP endpoint that sends one event and then keeps the response open.
- Subscribe with
client.event.subscribe(..., { signal })and consume the async stream so it is parked inreader.read(). - Abort the signal.
- Observe an unhandled
AbortErrorfrom the separate promise returned by the SDK'sreader.cancel()call.
Minimal outline:
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
process.on("unhandledRejection", (reason) => {
console.error("unhandled", reason)
process.exitCode = 1
})
const encoder = new TextEncoder()
const server = Bun.serve({
port: 0,
fetch() {
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('data: {"type":"server.connected"}\n\n'))
},
}),
{ headers: { "content-type": "text/event-stream" } },
)
},
})
const abort = new AbortController()
const client = createOpencodeClient({ baseUrl: `http://127.0.0.1:${server.port}` })
const response = await client.event.subscribe({}, { signal: abort.signal })
const consuming = (async () => {
for await (const _event of response.stream) {
// Keep consuming until aborted.
}
})()
await Bun.sleep(50)
abort.abort()
await consuming.catch(() => {})
await Bun.sleep(100)
server.stop(true)
Operating System
macOS 26.6.2 (arm64), Bun 1.4.0
Terminal
N/A (embedded SDK host process)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.