Skip to content

BunMultipart.stream hangs when the request body errors during an active file #8203

Description

@Chasewhip8

What version of Effect is running?

  • effect@4.0.0-rc.115
  • @effect/platform-bun@4.0.0-rc.115
  • Bun 1.4.2, Linux x64 (also reproduced on Bun 1.3.13)

What steps can reproduce the bug?

Create an empty directory with this package.json:

{
  "private": true,
  "type": "module",
  "dependencies": {
    "effect": "4.0.0-rc.115",
    "@effect/platform-bun": "4.0.0-rc.115"
  }
}

Save the following as repro.mjs. The request supplies a valid multipart file header and five file bytes, then its body stream errors while that file is active. highWaterMark: 0 keeps the error on the next pull rather than prefetching it before the buffered data is consumed.

import { Effect, Stream } from "effect";
import * as BunMultipart from "@effect/platform-bun/BunMultipart";

const body = new ReadableStream({
  start(controller) {
    controller.enqueue(new TextEncoder().encode(
      '--b\r\nContent-Disposition: form-data; name="file"; filename="a.txt"\r\n\r\nhello',
    ));
  },
  pull(controller) {
    controller.error(new Error("body-read-failed"));
  },
}, { highWaterMark: 0 });

const request = new Request("http://localhost/upload", {
  method: "POST",
  headers: { "content-type": "multipart/form-data; boundary=b" },
  body,
});

let bytesRead = 0;
const result = await Effect.runPromiseExit(
  BunMultipart.stream(request).pipe(
    Stream.runForEach((part) => part._tag === "File"
      ? Stream.runForEach(part.content, (chunk) => Effect.sync(() => { bytesRead += chunk.length; }))
      : Effect.void),
    Effect.timeout("1 second"),
  ),
);

console.log({ bun: Bun.version, bytesRead });
console.log(JSON.stringify(result, null, 2));

Run:

bun install --ignore-scripts
bun --no-install repro.mjs

What is the expected behavior?

Consuming the active file should promptly fail with the request-body read error, wrapped in MultipartError. The outer multipart-consumption effect should fail without reaching the watchdog timeout.

What do you see instead?

Five file bytes are consumed, then the operation fails only when the outer timeout expires:

{ bun: "1.4.2", bytesRead: 5 }

The resulting failure is TimeoutError, with the message Operation timed out after '1s', rather than the injected body-read-failed error.

As a control, consuming an identically constructed body through BunStream.fromReadableStream propagates the read error without timing out.

Additional information

In Multipart.makeChannel, the file-content pull loop checks buffered chunks and finished. pump records an upstream failure in exit, but the nested file loop does not check that value before pumping again. The outer part loop checks it, but the consumer is still awaiting the active file.

Related history: #7155 proposed propagating upstream failures to active files and was closed in favor of #7156. The latter addresses parser limits/truncated input. This reproduction uses an explicit request-body read failure with no configured parser limits and fails before any closing boundary is received. #6284 is a related limit-triggered hang report.

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