anomalyco / anomalyco/opencode

SDK SSE abort leaks an unhandled reader.cancel() rejection under Bun

Open
#47,963 1 comment 0 reactions 1 assignee View on GitHub

@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.ts
  • packages/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

  1. Start a real SSE HTTP endpoint that sends one event and then keeps the response open.
  2. Subscribe with client.event.subscribe(..., { signal }) and consume the async stream so it is parked in reader.read().
  3. Abort the signal.
  4. Observe an unhandled AbortError from the separate promise returned by the SDK's reader.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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.