What version of Effect is running?
effect@4.0.0-beta.99
What steps can reproduce the bug?
Create a small project:
npm init -y
npm pkg set type=module
npm install effect@4.0.0-beta.99 @effect/platform-node@4.0.0-beta.99 tsx
Add src.ts:
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema } from "effect"
import { createServer } from "node:http"
import { HttpRouter, Multipart } from "effect/unstable/http"
import {
HttpApi,
HttpApiBuilder,
HttpApiEndpoint,
HttpApiGroup,
HttpApiSchema,
} from "effect/unstable/httpapi"
const UploadPayload = Schema.Struct({
file: Multipart.SingleFileSchema,
}).pipe(
HttpApiSchema.asMultipart({
maxParts: 1,
maxFileSize: 1024,
maxTotalSize: 4096,
}),
)
class Uploads extends HttpApiGroup.make("uploads").add(
HttpApiEndpoint.post("upload", "/upload", {
payload: UploadPayload,
success: Schema.String,
}),
) {}
class Api extends HttpApi.make("api").add(Uploads) {}
const UploadsLive = HttpApiBuilder.group(
Api,
"uploads",
Effect.fn(function* (handlers) {
return handlers.handle("upload", ({ payload }) =>
Effect.sync(() => {
console.log("HANDLER INVOKED", payload.file.name)
return "ok"
}),
)
}),
)
const RoutesLive = HttpApiBuilder.layer(Api).pipe(
Layer.provide(UploadsLive),
)
const ServerLive = HttpRouter.serve(RoutesLive).pipe(
Layer.provide(
NodeHttpServer.layer(createServer, {
port: 32145,
}),
),
)
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
Start the server:
In another terminal, create files below and above the configured limit:
head -c 10 /dev/zero > small.txt
head -c 2048 /dev/zero > large.txt
Upload the small file:
curl -i -F 'file=@small.txt;type=text/plain' \
http://127.0.0.1:32145/upload
This behaves correctly:
HTTP/1.1 200 OK
content-type: application/json
content-length: 4
"ok"
The server prints:
HANDLER INVOKED small.txt
Upload the oversized file:
curl -i -F 'file=@large.txt;type=text/plain' \
http://127.0.0.1:32145/upload
This returns:
HTTP/1.1 500 Internal Server Error
The response body is empty, the endpoint handler is not invoked, and the server logs:
MultipartError: FileTooLarge
...
'http.status': 500
What is the expected behavior?
Exceeding a configured file or request size limit is an expected client-caused request failure and should not become an unconditional 500 defect.
A sensible default would be:
FileTooLarge / BodyTooLarge → 413 Payload Too Large
TooManyParts / FieldTooLarge / Parse → an appropriate 4xx response
InternalError → defect / 500
Alternatively, HttpApi could expose multipart parsing failures through a request-decoding error transformation hook, allowing applications to choose their own response.
I am not expecting the endpoint handler to run after buffered multipart decoding fails. The issue is that the failure is converted into a defect before any supported HTTP API error transformation can handle it.
What do you see instead?
Buffered multipart decoding currently contains:
let eff = Effect.orDie(httpRequest.multipart)
if (existing.limits) {
eff = Effect.provideContext(
eff,
Multipart.limitsServices(existing.limits),
)
}
return Effect.flatMap(eff, decode)
Because httpRequest.multipart is wrapped in Effect.orDie, every MultipartError becomes a defect before payload schema decoding and before the endpoint handler.
This includes expected client-caused failures such as FileTooLarge, even though MultipartError distinguishes them from InternalError.
HttpApiMiddleware.layerSchemaErrorTransform does not provide an escape hatch because it handles HttpApiSchemaError, while the multipart failure has already been converted into a defect.
Additional information
Workarounds
HttpApiSchema.asMultipartStream preserves MultipartError in the stream error channel, so an endpoint can consume the stream and map the error manually.
However, switching to the streaming API means giving up the buffered API's schema-decoded SingleFileSchema and scoped PersistedFile input. The application must manually consume, validate, and potentially persist multipart parts.
An application-level byte counter is another partial workaround, but it duplicates the multipart parser limit and cannot handle errors raised before the endpoint receives the upload.
Additional information
This is related to, but distinct from, #6392.
#6392 fixed multipart limit violations being silently swallowed depending on request chunking. The fix is present in beta.99: this reproduction confirms that FileTooLarge is now raised reliably.
This issue concerns what happens afterward: buffered HttpApi converts that correctly raised request error into a defect and returns 500, with no supported path to a 413 or another application-defined response.
What version of Effect is running?
effect@4.0.0-beta.99
What steps can reproduce the bug?
Create a small project:
npm init -y npm pkg set type=module npm install effect@4.0.0-beta.99 @effect/platform-node@4.0.0-beta.99 tsxAdd
src.ts:Start the server:
In another terminal, create files below and above the configured limit:
Upload the small file:
curl -i -F 'file=@small.txt;type=text/plain' \ http://127.0.0.1:32145/uploadThis behaves correctly:
The server prints:
Upload the oversized file:
curl -i -F 'file=@large.txt;type=text/plain' \ http://127.0.0.1:32145/uploadThis returns:
HTTP/1.1 500 Internal Server ErrorThe response body is empty, the endpoint handler is not invoked, and the server logs:
What is the expected behavior?
Exceeding a configured file or request size limit is an expected client-caused request failure and should not become an unconditional 500 defect.
A sensible default would be:
FileTooLarge/BodyTooLarge→413 Payload Too LargeTooManyParts/FieldTooLarge/Parse→ an appropriate 4xx responseInternalError→ defect / 500Alternatively,
HttpApicould expose multipart parsing failures through a request-decoding error transformation hook, allowing applications to choose their own response.I am not expecting the endpoint handler to run after buffered multipart decoding fails. The issue is that the failure is converted into a defect before any supported HTTP API error transformation can handle it.
What do you see instead?
Buffered multipart decoding currently contains:
Because
httpRequest.multipartis wrapped inEffect.orDie, everyMultipartErrorbecomes a defect before payload schema decoding and before the endpoint handler.This includes expected client-caused failures such as
FileTooLarge, even thoughMultipartErrordistinguishes them fromInternalError.HttpApiMiddleware.layerSchemaErrorTransformdoes not provide an escape hatch because it handlesHttpApiSchemaError, while the multipart failure has already been converted into a defect.Additional information
Workarounds
HttpApiSchema.asMultipartStreampreservesMultipartErrorin the stream error channel, so an endpoint can consume the stream and map the error manually.However, switching to the streaming API means giving up the buffered API's schema-decoded
SingleFileSchemaand scopedPersistedFileinput. The application must manually consume, validate, and potentially persist multipart parts.An application-level byte counter is another partial workaround, but it duplicates the multipart parser limit and cannot handle errors raised before the endpoint receives the upload.
Additional information
This is related to, but distinct from, #6392.
#6392 fixed multipart limit violations being silently swallowed depending on request chunking. The fix is present in beta.99: this reproduction confirms that
FileTooLargeis now raised reliably.This issue concerns what happens afterward: buffered
HttpApiconverts that correctly raised request error into a defect and returns 500, with no supported path to a 413 or another application-defined response.