Summary
Bun.serve() in src/server/index.ts never sets maxRequestBodySize, so Bun's default 128 MiB cap applies to the /v1/responses HTTP listener. When Codex posts a body larger than that, Bun rejects it at the HTTP layer before fetch() runs, returning 413 with an empty body. Codex renders an empty error body as the opaque string Unknown error — exactly the failure mode #452 and src/server/responses/passthrough-error.ts were written to prevent, except this one happens one layer below anything opencodex controls.
Client-visible symptom:
unexpected status 413 Payload Too Large: Unknown error, url: http://127.0.0.1:10100/v1/responses
Because the handler never runs, the request leaves no trace at all: no usage.jsonl row, no service.log line. From the operator's side the proxy looks perfectly healthy (ocx health → Proxy healthy) while every turn of the affected session fails.
Environment
- opencodex
2.10.0 (reproduced); 2.14.1 is still affected — see "Still present on latest" below
- Bun
1.3.14 (bundled)
- Codex CLI
0.147.0-alpha.6.5
- macOS (darwin arm64)
websockets: false in config.json, so Codex falls back to HTTP POST and always takes this path
Root cause
src/server/index.ts (2.10.0, L391):
const server: Server<WsData> = Bun.serve<WsData>({
port: listenPort,
hostname: bindHost,
idleTimeout: 255,
async fetch(req, requestServer): Promise<Response> {
No maxRequestBodySize here, and rg maxRequestBodySize src/ returns nothing anywhere in the package. Bun therefore applies its default of 128 MiB and answers oversized requests itself.
Every 413 opencodex itself produces carries a JSON envelope with a populated error.message:
formatErrorResponse → classifyError (src/lib/errors.ts) always preserves message
decodeRequestErrorResponse → 413 invalid_request_error for DecompressedBodyTooLargeError
413 request_too_large for TranslatorBudgetExceededError
formatPassthroughUpstreamError explicitly re-wraps empty upstream bodies into Provider error 413: (empty body)
So an empty-body 413 cannot originate from opencodex's own code paths. That is what makes the Bun layer identifiable as the source.
Evidence
-
No 413 anywhere in the proxy's own records. Status distribution across ~/.opencodex/usage.jsonl (10k+ requests):
200: 10766 499: 319 502: 145 429: 19 400: 6 402: 2
413: 0
service.log likewise has no 413 entry. The request never reached fetch().
-
The bodies really are over 128 MiB. Two Codex sessions that recorded the error, measured from their rollout JSONL:
| rollout |
file size |
response_item total |
image-bearing tool outputs |
…17-17-29-019ffa32 |
243 MB |
199.9 MB |
33 / 185.5 MB |
…13-40-27-019ff96b |
463 MB |
272.6 MB |
90 / 269.5 MB |
Individual custom_tool_call_output lines reach 25 MB / 9 MB / 3 MB — base64 images from Codex's built-in image_gen, replayed as conversation history on every turn. zstd request compression only recovers the base64 overhead (~75% of raw), so the wire body still lands around 150 MB and stays over the cap.
-
Both failures occurred on sub-agent turns — the error text is followed by This agent's turn failed. If you still need this agent, use the available collaboration tools to give it another task.
Reproduction
- Run a Codex session that accumulates >128 MiB of conversation input (easiest: dozens of
image_gen calls; any large multimodal history works).
- Route it through the opencodex proxy over HTTP (
websockets: false).
- The next turn fails with
unexpected status 413 Payload Too Large: Unknown error, and nothing is written to usage.jsonl or service.log.
Still present on latest
Checked the published 2.14.1 tarball. Bun.serve now takes a shared serveOptions object, which still does not set the field:
const serveOptions = {
idleTimeout: 255,
async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
rg maxRequestBodySize package/src/ → no matches. MAX_DECOMPRESSED_BODY_BYTES is still 256 * 1024 * 1024.
Impact
Suggested fix
Set maxRequestBodySize on Bun.serve to at least MAX_DECOMPRESSED_BODY_BYTES, so the documented 256 MiB cap is the one that actually fires and rejections come back as a typed JSON error rather than an empty 413:
const serveOptions = {
idleTimeout: 255,
maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
async fetch(...) { ... }
}
Optionally make it configurable alongside appOwnedMemoryBudgetMb, and log a warning when a request is rejected for size so the operator has something to find.
Note this is distinct from #987, which concerned an upstream gateway's 10 MiB cap. This one is the local listener rejecting the request before any provider is contacted.
Summary
Bun.serve()insrc/server/index.tsnever setsmaxRequestBodySize, so Bun's default 128 MiB cap applies to the/v1/responsesHTTP listener. When Codex posts a body larger than that, Bun rejects it at the HTTP layer beforefetch()runs, returning413with an empty body. Codex renders an empty error body as the opaque stringUnknown error— exactly the failure mode #452 andsrc/server/responses/passthrough-error.tswere written to prevent, except this one happens one layer below anything opencodex controls.Client-visible symptom:
Because the handler never runs, the request leaves no trace at all: no
usage.jsonlrow, noservice.logline. From the operator's side the proxy looks perfectly healthy (ocx health→Proxy healthy) while every turn of the affected session fails.Environment
2.10.0(reproduced);2.14.1is still affected — see "Still present on latest" below1.3.14(bundled)0.147.0-alpha.6.5websockets: falseinconfig.json, so Codex falls back to HTTP POST and always takes this pathRoot cause
src/server/index.ts(2.10.0, L391):No
maxRequestBodySizehere, andrg maxRequestBodySize src/returns nothing anywhere in the package. Bun therefore applies its default of 128 MiB and answers oversized requests itself.Every 413 opencodex itself produces carries a JSON envelope with a populated
error.message:formatErrorResponse→classifyError(src/lib/errors.ts) always preservesmessagedecodeRequestErrorResponse→413 invalid_request_errorforDecompressedBodyTooLargeError413 request_too_largeforTranslatorBudgetExceededErrorformatPassthroughUpstreamErrorexplicitly re-wraps empty upstream bodies intoProvider error 413: (empty body)So an empty-body 413 cannot originate from opencodex's own code paths. That is what makes the Bun layer identifiable as the source.
Evidence
No 413 anywhere in the proxy's own records. Status distribution across
~/.opencodex/usage.jsonl(10k+ requests):service.loglikewise has no 413 entry. The request never reachedfetch().The bodies really are over 128 MiB. Two Codex sessions that recorded the error, measured from their rollout JSONL:
response_itemtotal…17-17-29-019ffa32…13-40-27-019ff96bIndividual
custom_tool_call_outputlines reach 25 MB / 9 MB / 3 MB — base64 images from Codex's built-inimage_gen, replayed as conversation history on every turn. zstd request compression only recovers the base64 overhead (~75% of raw), so the wire body still lands around 150 MB and stays over the cap.Both failures occurred on sub-agent turns — the error text is followed by
This agent's turn failed. If you still need this agent, use the available collaboration tools to give it another task.Reproduction
image_gencalls; any large multimodal history works).websockets: false).unexpected status 413 Payload Too Large: Unknown error, and nothing is written tousage.jsonlorservice.log.Still present on latest
Checked the published
2.14.1tarball.Bun.servenow takes a sharedserveOptionsobject, which still does not set the field:rg maxRequestBodySize package/src/→ no matches.MAX_DECOMPRESSED_BODY_BYTESis still256 * 1024 * 1024.Impact
MAX_DECOMPRESSED_BODY_BYTESadmission cap the code intends to enforce. Requests between 128 MiB and 256 MiB are killed by Bun instead of reaching the guard that would explain itself.ocx health, zero log lines, zero usage rows.Unknown errorthat 503 from /v1/responses via Codex even though direct proxy curl to same endpoint returns 200 OK (Xiaomi MiMo provider, openai-chat adapter) #452's wrapping exists to eliminate.Suggested fix
Set
maxRequestBodySizeonBun.serveto at leastMAX_DECOMPRESSED_BODY_BYTES, so the documented 256 MiB cap is the one that actually fires and rejections come back as a typed JSON error rather than an empty 413:Optionally make it configurable alongside
appOwnedMemoryBudgetMb, and log a warning when a request is rejected for size so the operator has something to find.Note this is distinct from #987, which concerned an upstream gateway's 10 MiB cap. This one is the local listener rejecting the request before any provider is contacted.