Summary
When a single uploaded file exceeds MAX_FILE_SIZE, the API returns 413 (correct), but immediately afterwards an uncaught TypeError: Invalid state: Controller is already closed is thrown. The global uncaughtException handler then triggers gracefulShutdown(), so the API process exits and restarts. Every in-flight upload and execution for all users is lost, and the caller (LibreChat) gets Failed to provision 1 file(s) to the code environment.
Environment
main @ 337ddd6, self-hosted via docker compose, API on Bun (Dockerfile.api)
- default
MAX_FILE_SIZE (25 MiB); the upload was a ~27 MB PDF sent by LibreChat
Logs (reproducible: happened 4 times in a row with the same file, and the API restarted each time)
warn [...] Upload: Session ID: sS_Cqu0XzWPCy2C9Xr3-g | ...
warn [...] File size limit exceeded: Darlehensvertrag.pdf | Session: sS_Cqu0XzWPCy2C9Xr3-g
error Uncaught Exception Invalid state: Controller is already closed
TypeError: Invalid state: Controller is already closed
at enqueue (unknown)
at onData (internal:webstreams_adapters:289:29)
at emit (node:events:100:22)englischen Issue-Text schreiben, wenn du willst. (disable recaps in /config)
❯ ja
Ran 2 shell commands
Der lokale Stand ist aktuell. Ich lese die betroffenen Code-Stellen selbst nach, damit das Issue exakt zitiert.
Ran 1 shell command
Hier ist der Issue-Text zum Kopieren. Ich habe ihn gegen den aktuellen main geprüft (337ddd6, lokal identisch mit Upstream), die Code-Stellen sind wörtlich zitiert.
### `/upload`: an oversized file crashes the whole API process ("Controller is already closed")
**Summary**
When a single uploaded file exceeds `MAX_FILE_SIZE`, the API returns 413 (correct), but immediately afterwards an uncaught `TypeError: Invalid state: Controller is already closed` is thrown. The global `uncaughtException` handler then triggers `gracefulShutdown()`, so the API process exits and restarts. Every in-flight upload and execution for all users is lost, and the caller (LibreChat) gets `Failed to provision 1 file(s) to the code environment`.
**Environment**
- `v1.5.1` (cf0e668), also verified unchanged on `main` @ 337ddd6; self-hosted via docker compose, API on Bun (`Dockerfile.api`)
- default `MAX_FILE_SIZE` (25 MiB); the upload was a ~27 MB PDF sent by LibreChat
**Logs** (reproducible: happened 4 times in a row with the same file, and the API restarted each time)
warn [...] Upload: Session ID: sS_Cqu0XzWPCy2C9Xr3-g | ...
warn [...] File size limit exceeded: Darlehensvertrag.pdf | Session: sS_Cqu0XzWPCy2C9Xr3-g
error Uncaught Exception Invalid state: Controller is already closed
TypeError: Invalid state: Controller is already closed
at enqueue (unknown)
at onData (internal:webstreams_adapters:289:29)
at emit (node:events:100:22)
at (internal:streams/readable:376:45)
at flow (internal:streams/readable:604:57)
...
info Initiating graceful shutdown...
warn Post-process busboy error for session sS_Cqu0XzWPCy2C9Xr3-g: Unexpected end of form
info Graceful shutdown completed
info Starting API service (no workers)...
.toWeb(file)` (`service/src/service/router.ts` ~L67-87). On `limit`, the handler aborts the fetch and then resumes the same Node stream (`router.ts` ~L490-500):
```ts
file.on('limit', () => {
...
abortController.abort();
file.resume();
res.status(413).json({ error: 'File size limit exceeded' });
});
abort() cancels the fetch and closes the web ReadableStream created by Readable.toWeb. The node→web adapter is still listening for data on file. So when file.resume() (and busboy draining the remaining part) emits more data, the adapter calls controller.enqueue() on a closed controller and throws inside an event-emitter callback. No local handler catches it, so it reaches service/src/api-server.ts ~L81-84:
process.on('uncaughtException', async (error) => {
logger.error('Uncaught Exception', error);
await gracefulShutdown();
});
The same abort-then-resume pattern also appears in the /upload timeout and error paths (~L503-507, ~L557-560) and in the /upload/batch limit handler (~L701-705). Those paths are presumably affected too, though I only observed the single-file size-limit case.
Expected
The oversized file gets a 413 (or a per-file error in batch), and the process keeps running.
Possible fix directions
- Detach before draining: stop the web adapter from receiving further
data (e.g. file.unpipe()/remove listeners, or pipe into a sink stream), rather than calling resume() on the stream owned by Readable.toWeb.
- Or enforce the size limit on the web-stream side (a counting
TransformStream that errors the stream), and let busboy drain into a no-op sink.
- Defensively, don't shut the whole API down for errors that belong to a single reques
Workaround
Raise MAX_FILE_SIZE (it also has to be passed to the api service in compose, and i. This only moves the threshold.
Summary
When a single uploaded file exceeds
MAX_FILE_SIZE, the API returns 413 (correct), but immediately afterwards an uncaughtTypeError: Invalid state: Controller is already closedis thrown. The globaluncaughtExceptionhandler then triggersgracefulShutdown(), so the API process exits and restarts. Every in-flight upload and execution for all users is lost, and the caller (LibreChat) getsFailed to provision 1 file(s) to the code environment.Environment
main@ 337ddd6, self-hosted via docker compose, API on Bun (Dockerfile.api)MAX_FILE_SIZE(25 MiB); the upload was a ~27 MB PDF sent by LibreChatLogs (reproducible: happened 4 times in a row with the same file, and the API restarted each time)
warn [...] Upload: Session ID: sS_Cqu0XzWPCy2C9Xr3-g | ...
warn [...] File size limit exceeded: Darlehensvertrag.pdf | Session: sS_Cqu0XzWPCy2C9Xr3-g
error Uncaught Exception Invalid state: Controller is already closed
TypeError: Invalid state: Controller is already closed
at enqueue (unknown)
at onData (internal:webstreams_adapters:289:29)
at emit (node:events:100:22)
at (internal:streams/readable:376:45)
at flow (internal:streams/readable:604:57)
...
info Initiating graceful shutdown...
warn Post-process busboy error for session sS_Cqu0XzWPCy2C9Xr3-g: Unexpected end of form
info Graceful shutdown completed
info Starting API service (no workers)...
abort()cancels the fetch and closes the webReadableStreamcreated byReadable.toWeb. The node→web adapter is still listening fordataonfile. So whenfile.resume()(and busboy draining the remaining part) emits more data, the adapter callscontroller.enqueue()on a closed controller and throws inside an event-emitter callback. No local handler catches it, so it reachesservice/src/api-server.ts~L81-84:The same abort-then-resume pattern also appears in the
/uploadtimeout and error paths (~L503-507, ~L557-560) and in the/upload/batchlimit handler (~L701-705). Those paths are presumably affected too, though I only observed the single-file size-limit case.Expected
The oversized file gets a 413 (or a per-file error in batch), and the process keeps running.
Possible fix directions
data(e.g.file.unpipe()/remove listeners, or pipe into a sink stream), rather than callingresume()on the stream owned byReadable.toWeb.TransformStreamthat errors the stream), and let busboy drain into a no-op sink.Workaround
Raise
MAX_FILE_SIZE(it also has to be passed to theapiservice in compose, and i. This only moves the threshold.