fetch with a streamed body (duplex: "half") hangs or ends cleanly with a truncated body when the response stream errors · Issue #46219 · oven-sh/bun · GitHub
Skip to content

fetch with a streamed body (duplex: "half") hangs or ends cleanly with a truncated body when the response stream errors #46219

Open

Description

@daniellmartins

What version of Bun is running?

1.4.2+744846f84

What platform is your computer?

Darwin 27.0.1 arm64 (the same test also fails intermittently on GitHub's ubuntu-latest runners)

What steps can reproduce the bug?

A server echoes a streamed request body through a TransformStream that errors once more than LIMIT bytes have
passed. A client streams LIMIT + 1 bytes with fetch(url, { method: "POST", body: ReadableStream, duplex: "half" })
and reads the response body.

server.ts:

const LIMIT = Number(process.env.LIMIT ?? 128 * 1024 * 1024);
const server = Bun.serve({
  port: 0,
  fetch(req) {
    let seen = 0;
    return new Response(
      req.body!.pipeThrough(
        new TransformStream<Uint8Array, Uint8Array>({
          transform(chunk, c) {
            seen += chunk.byteLength;
            if (seen > LIMIT) c.error(new Error("Request body too large"));
            else c.enqueue(chunk);
          },
        }),
      ),
    );
  },
});
console.log(server.port);

client.ts (run as bun client.ts http://127.0.0.1:<port>/ <LIMIT + 1>):

const [url, total] = [process.argv[2], Number(process.argv[3])];
const chunk = new Uint8Array(1024 * 1024);
let sent = 0;
const body = new ReadableStream({
  pull(c) {
    if (sent >= total) return c.close();
    const n = Math.min(chunk.length, total - sent);
    sent += n;
    c.enqueue(n === chunk.length ? chunk : chunk.subarray(0, n));
  },
});
let got = 0;
try {
  const r = await fetch(url, { method: "POST", body, duplex: "half" } as RequestInit);
  for await (const part of r.body!) got += part.byteLength;
} catch {
  got = -1;
}
console.log(got);

Run the client repeatedly against one server, with nothing else loading the machine.

What is the expected behavior?

The response body breaks: the for await (or the fetch) rejects, so the client prints -1, every time.

What do you see instead?

Two wrong outcomes, intermittently:

LIMIT tries rejects (-1) hangs (no end after 20 s) ends cleanly with a truncated body
3 000 000 30 21 9 0
128 MiB 30 26 0 4 (e.g. 133 887 564 bytes, fewer than the limit)
  • Hang: the client's fetch or body read never settles; the process stays alive until killed.
  • Truncated clean end: the body iterator finishes normally with fewer bytes than were sent, with no error, so the
    client cannot tell a cut-off answer from a complete one.

With curl as the client against the same server (head -c 3000001 /dev/zero | curl -T - -H "Transfer-Encoding: chunked" -X POST …), 20 of 20 runs fail with a connection error (exit 56, once 55) and none hang, which suggests
the server side aborts correctly and the issue is in fetch's handling of a response that aborts while a
streamed (duplex: "half") request body is still being sent.

Additional information

Under CPU load the hang is the common failure; a test that streams just past a 128 MiB limit times out there.

Activity

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