Skip to content

node:http2: @cursor/sdk local agent stream fails with NGHTTP2_FRAME_SIZE_ERROR in Bun #31499

Closed as not planned

Description

@xiehaibin18

What version of Bun is running?

1.4.0-canary.1+346ce0805

What platform is your computer?

Darwin 25.5.0 arm64

What steps can reproduce the bug?

This reproduces with @cursor/[email protected] when running a local Cursor SDK agent against a git workspace. The same script succeeds in Node.js.

mkdir /tmp/cursor-sdk-bun-http2-repro
cd /tmp/cursor-sdk-bun-http2-repro
bun init -y
bun add @cursor/sdk

Create repro.mjs:

import { execFileSync } from "node:child_process";
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { Agent } from "@cursor/sdk";

const apiKey = process.env.CURSOR_API_KEY;
if (!apiKey) {
  throw new Error("Set CURSOR_API_KEY before running this repro");
}

const cwd = await mkdtemp(join(tmpdir(), "cursor-sdk-git-repro-"));
await writeFile(
  join(cwd, "README.md"),
  "# Cursor SDK Bun HTTP/2 repro\n\nTiny local git repo for reproducing the stream failure.\n",
  "utf8",
);
execFileSync("git", ["init"], { cwd, stdio: "inherit" });
execFileSync("git", ["add", "README.md"], { cwd, stdio: "inherit" });
execFileSync(
  "git",
  ["-c", "user.name=Repro", "-c", "[email protected]", "commit", "-m", "init"],
  { cwd, stdio: "inherit" },
);

const agent = await Agent.create({
  apiKey,
  model: { id: "composer-2" },
  local: { cwd },
});

const run = await agent.send(
  "Read README.md and answer in one short sentence. Do not modify files.",
);
console.log(JSON.stringify({ event: "started", agentId: agent.agentId, runId: run.id }));

for await (const message of run.stream()) {
  console.log(
    JSON.stringify({
      event: "stream",
      type: message.type,
      status: message.status,
      name: message.name,
      text: message.message?.content?.[0]?.text,
    }),
  );
}

const result = await run.wait();
console.log(JSON.stringify({ event: "result", status: result.status, result: result.result }));

Run with Bun:

CURSOR_API_KEY=... bun repro.mjs

For comparison, run the same script with Node.js:

CURSOR_API_KEY=... node repro.mjs

What is the expected behavior?

Bun should be able to run the Cursor SDK local agent stream the same way Node.js does. The stream should deliver tool/assistant/status events and finish successfully.

What do you see instead?

With Bun, the run starts and usually emits an initial RUNNING status event, then the node:http2 stream fails with:

ConnectError: [internal] Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR
 rawMessage: "Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR",
       code: 13,
   metadata: Headers {},
    details: [],

      at from (.../node_modules/@connectrpc/connect/dist/esm/connect-error.js:71:20)
      at connectErrorFromNodeReason (.../node_modules/@connectrpc/connect-node/dist/esm/node-error.js:52:29)
      at reject (.../node_modules/@connectrpc/connect-node/dist/esm/node-universal-client.js:293:63)
      at h2StreamError (.../node_modules/@connectrpc/connect-node/dist/esm/node-universal-client.js:182:22)

error: Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR
 code: "ERR_HTTP2_STREAM_ERROR"

Bun v1.4.0-canary.1+346ce0805 (macOS arm64)

With Node.js v24.16.0, the exact same script and workspace stream successfully and return a finished result.

Additional information

This may be related to #25589, but that issue is closed and was about a generic gRPC request/frame-size regression. This repro is specifically for @cursor/sdk local agents, which use @connectrpc/connect-node / node:http2 streaming internally.

One observed detail: if the local workspace is a plain temporary directory, Bun may complete successfully. Initializing the workspace as a git repository makes the local Cursor SDK agent path consistently hit the HTTP/2 stream failure in this repro.

Activity

  1. github-actions commented on May 28, 2026

    @github-actions
    Contributor

    Found 2 possible duplicate issues:

    1. HTTP/2 gRPC requests fail with NGHTTP2_FRAME_SIZE_ERROR after v1.2.15 #25589
    2. HTTP/2 Flow Control Bug in node:http2 - Requests Hang Indefinitely #30342

    This issue will be automatically closed as a duplicate in 3 days.

    • If your issue is a duplicate, please close it and 👍 the existing issue instead
    • To prevent auto-closure, add a comment or 👎 this comment

    🤖 Generated with Claude Code

  2. xiehaibin18 commented on May 28, 2026

    @xiehaibin18
    Author

    Additional data point: I also reproduced this with the current stable Bun release, not only canary.

    • Bun stable: 1.3.14+0d9b296af
    • Node comparison: v24.16.0
    • Package: @cursor/[email protected]
    • Platform: Darwin 25.5.0 arm64

    Using the same temporary git workspace and the same SDK script:

    • [email protected] starts the run and emits initial stream events, but repeatedly logs ConnectError: [internal] Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR from @connectrpc/connect-node / node:http2. The run eventually returns FINISHED, but the agent reports that file/shell access failed and it cannot read README.md.
    • [email protected] on the exact same workspace streams normally: the read tool call completes, assistant text is streamed, and the run returns a successful finished result.

    So the repro is not limited to 1.4.0-canary.1; stable 1.3.14 also shows the HTTP/2 stream/tool-call failure for this Cursor SDK local git workspace path.

  3. robobun commented on May 28, 2026

    @robobun
    Collaborator

    Unable to reproduce this.

    Could not reproduce the NGHTTP2_FRAME_SIZE_ERROR in a self-contained HTTP/2 test

    Reported on: 1.4.0-canary.1+346ce0805 (macOS arm64)
    Tested with: debug build of main @ 3ec6669844, and Node.js v24.3.0, on linux x64

    The reported failure only happens inside the real @cursor/sdk local-agent flow, which needs a paid CURSOR_API_KEY and a live connection to Cursor's relay server. I don't have that credential, so I couldn't run the exact script. Instead I tried to reproduce the underlying node:http2 client behavior that @connectrpc/connect-node exercises. In every self-contained reproduction, Bun behaved identically to Node and did not produce NGHTTP2_FRAME_SIZE_ERROR.

    What the error actually is

    @connectrpc/connect-node surfaces this in h2StreamError (its stream.on("error", ...) handler). It falls through to the default Code.Internal (= code: 13, matching the [internal] prefix in the report). Bun raises ERR_HTTP2_STREAM_ERROR with message Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR when a stream is closed with RST code 0x6 (FRAME_SIZE_ERROR). For an inbound DATA frame that is itself too large, Bun sends a connection-level GOAWAY, not a stream RST — so the most likely origin of the report is an RST_STREAM(FRAME_SIZE_ERROR) received from the Cursor relay server, i.e. the server rejecting a specific frame Bun sent.

    Attempt 1 — large streamed POST body (h2c), Bun client → Node server

    Server on default settings (maxFrameSize: 16384); client writes 50 KB + 20 KB + 5 KB then end().

    $ bun-debug client.mjs <port>
    OK status=200 body="received 75000 bytes\n"

    Node baseline is identical. Bun correctly splits the body into ≤16 KB DATA frames.

    Attempt 2 — body-size sweep across the 16384-byte frame boundary

    Single req.end(Buffer.alloc(size)) for each size, Bun client → Node server:

    OK size=16383 -> received 16383 bytes
    OK size=16384 -> received 16384 bytes
    OK size=16385 -> received 16385 bytes
    OK size=16393 -> received 16393 bytes
    OK size=16394 -> received 16394 bytes
    OK size=32768 -> received 32768 bytes
    OK size=65535 -> received 65535 bytes
    OK size=65536 -> received 65536 bytes
    OK size=100000 -> received 100000 bytes

    No frame-size error at any boundary.

    Attempt 3 — many queued writes + PADDING_STRATEGY_MAX

    2000 rapid req.write() calls (≈1.29 MB) without waiting for drain, forcing the internal queue/flush_queue splitting path, with max padding enabled:

    $ bun-debug client3.mjs <port>
    client queued total= 1289000
    OK -> received 1289000 bytes

    Node baseline identical. The queued/backpressure DATA path and padding stay within the frame-size limit.

    Attempt 4 — TLS (h2) + server streaming + gRPC-style trailers

    This mirrors connectrpc: TLS createSecureServer, server writes ~1.2 MB in 4 KB chunks then sendTrailers({ "grpc-status": "0" }); Bun client reads response + trailers, sends an 8 KB request body.

    $ bun-debug tls-client.mjs <port> 8000
    OK status=200 recv=1228800 trailers={"grpc-status":"0","grpc-message":"ok"}

    Node baseline identical. TLS h2, server-streaming, and trailing HEADERS all work.

    Conclusion

    Bun's generic node:http2 client framing — DATA splitting, HEADERS/CONTINUATION (the code consults the peer's SETTINGS_MAX_FRAME_SIZE), trailers, TLS, backpressure queuing, and padding — is correct in every self-contained test I could build, and matches Node.js. The NGHTTP2_FRAME_SIZE_ERROR is a stream reset whose trigger lives in the live @cursor/sdk ↔ Cursor-relay exchange, which I can't reproduce here without a CURSOR_API_KEY and knowledge of the exact frame sequence the server rejects.

    To move this forward, the most useful thing would be an HTTP/2 frame trace so we can see which frame Bun sends that the server rejects. Please capture one of:

    • NODE_DEBUG_NATIVE=http2 (or NODE_DEBUG=http2) output from the failing Bun run, and from the passing Node run, so the two frame streams can be compared; or
    • a tcpdump/Wireshark capture of the h2 session (decrypt with SSLKEYLOGFILE), or
    • BUN_DEBUG_H2FrameParser=1 bun repro.mjs 2>h2.log (Bun's internal HTTP/2 frame log) from a failing run.

    The specific frame sizes involved (especially around the larger request content a git workspace produces) would let us pinpoint the divergence. A smaller self-contained repro that doesn't need the Cursor API — even one that reproduces intermittently — would also unblock this.

    If you can provide a minimal reproduction (exact command, Bun version, platform), please update the issue.


    Automated investigation. Reopen with more context if this is wrong.

  4. xiehaibin18 commented on May 28, 2026

    @xiehaibin18
    Author

    I ran a smaller differential trace against the same tiny local git repo with @cursor/sdk local agent. Summary: Bun still fails with NGHTTP2_FRAME_SIZE_ERROR; Node succeeds on the same fixture.

    Environment:

    Repro shape:

    const agent = await Agent.create({
      apiKey,
      model: { id: "composer-2" },
      local: { cwd }, // tiny temp git repo with README.md
    });
    
    const run = await agent.send(
      "Read README.md and answer in one short sentence. Do not modify files.",
    );
    
    for await (const message of run.stream()) {
      console.log(JSON.stringify({
        event: "stream",
        type: message.type,
        status: message.status,
        name: message.name,
      }));
    }
    
    const result = await run.wait();
    console.log(JSON.stringify({ event: "result", status: result.status, result: result.result }));

    Commands used:

    BUN_DEBUG_H2FrameParser=1 npm exec --yes [email protected] -- repro.mjs \
      > bun-stable-stdout.log 2> bun-stable-h2-stderr.log
    
    CURSOR_TRACE_CWD=<same-temp-git-repo> \
    NODE_DEBUG_NATIVE=http2 NODE_DEBUG=http2 node repro.mjs \
      > node-stdout.log 2> node-http2-stderr.log

    Bun result, from stdout:

    {"event":"stream","type":"tool_call","status":"running","name":"read"}
    {"event":"stream","type":"tool_call","status":"running","name":"glob"}
    {"event":"stream","type":"tool_call","status":"running","name":"shell"}
    ...
    {"event":"stream","type":"status","status":"FINISHED"}
    {"event":"result","status":"finished","result":"I couldn’t read `README.md` here because file and shell access failed in this environment, so I can’t summarize it yet."}
    

    Bun stderr repeatedly shows the same Connect error:

    ConnectError: [internal] Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR
     rawMessage: "Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR",
    ...
    error: Stream closed with error code NGHTTP2_FRAME_SIZE_ERROR
    

    Node result on the same temp git repo:

    {"event":"stream","type":"assistant","text":"Reading"}
    {"event":"stream","type":"assistant","text":" README"}
    {"event":"stream","type":"tool_call","status":"running","name":"read"}
    {"event":"stream","type":"tool_call","status":"completed","name":"read"}
    {"event":"stream","type":"status","status":"FINISHED"}
    {"event":"result","status":"finished","result":"This is a small local git repo for reproducing and comparing HTTP/2 behavior between Bun and Node."}
    

    Node NODE_DEBUG_NATIVE=http2 NODE_DEBUG=http2 sample, showing normal HTTP/2 progress and stream close with code 0:

    HTTP2 <pid>: Http2Session client: sending settings
    HTTP2 <pid>: Http2Session client: submitting settings
    Http2Session client (...) nghttp2 has 24 bytes to send
    Http2Session client (...) nghttp2 has 9 bytes to send
    Http2Session client (...) nghttp2 has 493 bytes to send
    Http2Session client (...) sending 2 bytes for data frame on stream 1
    ...
    Http2Session client (...) complete frame received: type: 4
    Http2Session client (...) handling settings frame
    Http2Session client (...) complete frame received: type: 8
    ...
    Http2Session client (...) complete frame received: type: 1
    Http2Session client (...) handle headers frame for stream 1
    Http2Session client (...) complete frame received: type: 0
    Http2Session client (...) handling data frame for stream 1
    Http2Session client (...) stream 1 closed with code: 0
    HTTP2 <pid>: Http2Stream 1 [Http2Session client]: closed with code 0, closed false, readable false
    

    One note: in this npm-installed [email protected] run, BUN_DEBUG_H2FrameParser=1 did not appear to emit the low-level frame parser rows you asked for. The Bun failure is still visible at the Cursor SDK / ConnectError layer, while Node's native HTTP/2 debug trace shows the same local-agent task completing successfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions