Skip to content

fix(http): fail active multipart file parts on upstream body failure - #8207

Closed
mnkprs wants to merge 1 commit into
Effect-TS:mainfrom
mnkprs:fix/multipart-upstream-failure
Closed

mnkprs wants to merge 1 commit into
Effect-TS:mainfrom
mnkprs:fix/multipart-upstream-failure

Conversation

@mnkprs

@mnkprs mnkprs commented Sep 12, 2026 •

Copy link
Copy Markdown

Fixes #8203

Summary

  • fail an active multipart file part when the request body stream errors, instead of pumping a dead upstream forever
  • normalize the upstream cause to a MultipartError so File.content and File.contentEffect keep their declared error type
  • add regression tests for a failing body during an active file, for contentEffect, and for a file part that is never consumed
  • add a patch changeset

Root cause

In Multipart.makeChannel, pump records an upstream failure in exit and the outer part loop checks it, but the per-file pull loop created in onFile only checked its own finished flag:

return finished ? Cause.done() : Effect.flatMap(pump, loop)

The parser is never told about an upstream failure (parser.end() only runs on Done), so it cannot send the active file callback its terminating null. finished therefore stays false and a consumer draining part.content calls pump on an already-failed upstream forever. The loop never yields, so the failure surfaces only when an outer watchdog fires, and in the tests here even Effect.timeout inside the same fiber never gets a chance to run.

This is a different trigger from #7155 / #7156. Those covered parser limit and truncated-input failures, which the parser can observe and which it now terminates by sending null to the active file. An upstream stream failure is invisible to the parser, so it has to be handled in the wrapper.

Change

makeChannel keeps the upstream cause in a new upstreamFailure slot next to the existing exit, mapped so a cause that is already a MultipartError passes through and anything else becomes InternalError. The file pull loop drains any buffered chunks first, then ends on finished, and otherwise fails with that cause rather than pumping again.

Because the file channel can now fail, its error type widens from never to MultipartError. This is internal only: the public File.content and File.contentEffect were already typed as failing with MultipartError. FileImpl.contentEffect therefore no longer needs to wrap the channel error, and defaultWriteFile now passes an existing MultipartError through instead of re-wrapping it as InternalError.

A file that completed before the failure still ends cleanly, so the parser-limit behavior from #7156 is unchanged.

Validation

  • pnpm test --run packages/effect/test/unstable/http/Multipart.test.ts (19 tests)
  • the two new consuming tests hang and time out on main, and pass with the change
  • pnpm test --run packages/effect/test/unstable/http/HttpServerRequest.test.ts packages/effect/test/unstable/http/HttpServer.test.ts packages/effect/test/unstable/http/HttpPlatform.test.ts
  • pnpm test --run packages/platform/node/test/MultipartParser.test.ts packages/platform/node/test/HttpApi.test.ts (128 tests)
  • pnpm lint, pnpm check

The issue reproduces through @effect/platform-bun, but the hang is in the shared effect/unstable/http/Multipart channel that BunMultipart.stream pipes through, so the regression tests live with the core module. Bun was not available locally to run the reporter's script directly.

When the request body stream fails while a file part is being consumed,
the parser never learns about it, so it cannot terminate the active file
callback. The file's pull loop only checked its `finished` flag and kept
pumping an already-failed upstream forever, starving the fiber until an
outer timeout fired.

Record the upstream cause alongside the existing part-stream exit and
fail the active file part with it. Causes that are already a
`MultipartError` pass through unchanged, anything else is wrapped as
`InternalError`, so `File.content` and `File.contentEffect` keep their
declared error type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pVAxS811pzbc8NJtmvCWs
@changeset-bot

changeset-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3b560c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@mnkprs

mnkprs commented Sep 12, 2026

Copy link
Copy Markdown
Author

The Types failure looks like a runner OOM rather than a type error: tsc -b tsconfig.json printed only Killed and exited 137, with no diagnostic, and the deno check / test-types steps were skipped.

The same job failed identically on a push to main before this PR existed (run 34657889492). pnpm check passes locally on this commit, and every other job here is green, including all four test runtimes.

Could someone re-run that job when convenient?

@mnkprs

mnkprs commented Sep 14, 2026

Copy link
Copy Markdown
Author

Superseded by #8206, which landed the same fix. Closing.

@mnkprs mnkprs closed this Sep 14, 2026

This branch is waiting to be deployed

1 waiting deployment
fork — 3b560c71 Waiting Sep 12, 2026 by mnkprs via approval-gate #14858
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.0 bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant