Skip to content

[Bug] Bun.serve has no maxRequestBodySize: >128 MiB /v1/responses bodies get an empty-body 413 that Codex renders as "Unknown error" #1601

Closed

Description

@nekonade

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

  1. 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().

  2. 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.

  3. 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

  1. Run a Codex session that accumulates >128 MiB of conversation input (easiest: dozens of image_gen calls; any large multimodal history works).
  2. Route it through the opencodex proxy over HTTP (websockets: false).
  3. 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.

Activity

  1. coderabbitai commented on Aug 13, 2026

    @coderabbitai
    Contributor
    🔗 Related PRs

    #463 - fix(proxy): stop empty 503 bodies becoming Codex Unknown error (#452) [merged]
    #599 - fix: scope Spark cooldowns to its native quota [merged]
    #749 - fix(responses): reject expired forward continuations [merged]
    #922 - fix(codex): isolate provider host transport health [open]
    #1412 - fix(responses): refuse oversized input and stop compounding replayed history [open]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. github-actions commented on Aug 13, 2026

    @github-actions
    Contributor

    Maintainer decision respected

    A maintainer has reopened this issue. The automated closure has been deactivated.

  3. reopened this on Aug 13, 2026
  4. lidge-jun commented on Aug 13, 2026

    @lidge-jun
    Owner

    Confirmed from the current dev code: this is a valid defect and is not fixed on dev.

    src/server/request-decompress.ts:20 defines MAX_DECOMPRESSED_BODY_BYTES as 256 MiB, but the shared Bun.serve options at src/server/index.ts:727-729 set only idleTimeout. Both the public listener and the optional loopback listener spread those options at src/server/index.ts:1638-1650, so neither raises Bun's default body limit. The /v1/responses handler begins only after the listener accepts the request (src/server/index.ts:1188-1216), which supports your conclusion that an over-limit request is rejected before OpenCodex can format or log its own error response.

    Thank you for the unusually complete report — the usage.jsonl status distribution and the per-rollout byte accounting made the 128 MiB boundary identifiable without a reproduction on our side.

    Setting maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES on the shared options is the correct narrow fix. Linked PR #1636 has that production change but needs a real request in the 128-256 MiB range plus deterministic environment/teardown cleanup before merge; its current bodyless health check cannot prove this failure mode.

    DISPOSITION: ACCEPTED

  5. lidge-jun commented on Aug 14, 2026

    @lidge-jun
    Owner

    Fixed by cherry-picking #1636 (fix(server): configure maxRequestBodySize to 256 MiB on Bun.serve listeners) onto dev at 50fe0d4e9.

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions