Skip to content

Proxy wraps backend 404 as 400 "bad request: 404 Not Found" #3854

@radisicc

Description

@radisicc

[Bug] Proxy wraps backend 404 as 400 "bad request: 404 Not Found"

Labels: bug, proxy, dx


Description

When a backend application inside a sandbox returns a legitimate 404 Not Found response, the Daytona proxy (*.proxy.daytona.works) wraps it and delivers it to the client as a 400 Bad Request with the following body:

{"statusCode": 400, "message": "bad request: 404 Not Found", "code": "BAD_REQUEST"}

This means clients cannot distinguish between:

  • A genuine application-level 404 (e.g. session not found, resource missing)
  • A proxy-level error (misconfiguration, connection failure)

It also causes SDK and client error-handling code to misclassify the error — a 404 should be catchable as "not found" but instead surfaces as a 400 Bad Request.


Reproduction Steps

  1. Run any HTTP server inside a sandbox on any port (e.g. port 4096)
  2. Make the server return 404 for a specific path:
    # minimal example
    self.send_response(404)
    self.send_header("Content-Type", "application/json")
    body = b'{"error": "not found"}'
    self.send_header("Content-Length", str(len(body)))
    self.end_headers()
    self.wfile.write(body)
  3. Request that path through the preview proxy:
    GET https://{port}-{token}.proxy.daytona.works/missing-resource
    
  4. Observe response — client receives 400, not 404:
    {"statusCode": 400, "message": "bad request: 404 Not Found", "code": "BAD_REQUEST"}

Expected Behavior

The proxy should pass through the backend's HTTP status code unchanged. A backend 404 should reach the client as a 404.


Actual Behavior

The proxy intercepts the backend 404 response and re-emits it as 400 BAD_REQUEST. The original status code is embedded in the error message string as "bad request: 404 Not Found".


Real-World Impact

This was observed with two different application stacks:

Case 1 — opencode server (port 4096)

opencode stores sessions and returns 404 when a session ID is not found:

POST /session/{id}/prompt_async  →  404 (session not found)

The client receives:

{"statusCode": 400, "message": "bad request: 404 Not Found", "code": "BAD_REQUEST"}

The @opencode-ai/sdk TypeScript client catches this as a BadRequestError instead of a NotFoundError, making it impossible to write correct error-handling or retry logic.

Case 2 — Vite dev server (port 5173)

After the daemon's pooled connection to Vite goes stale (Vite closes it after its 5s keepAliveTimeout), the daemon attempts to reuse the dead connection. The resulting connection-reset error is also surfaced to the client as the same 400 "bad request: 404 Not Found" message — even though the server is running and the error is a proxy-internal connection failure.

Both errors produce identical output from the client's perspective, making diagnosis extremely difficult.


Connection Architecture

Client ──HTTP/2──> Daytona Cloud Proxy (*.proxy.daytona.works)
                        │
                        │  persistent TCP connection
                        ▼
                   Daytona Daemon inside sandbox (:2280)
                        │
                        │  HTTP/1.1 pooled connection
                        ▼
                   App server (e.g. :4096 or :5173)

Root Cause

The Daytona proxy error middleware wraps certain backend errors using a pattern equivalent to:

NewBadRequestError(fmt.Sprintf("bad request: %s", err.Error()))

When the backend returns a 404, the HTTP status line "404 Not Found" becomes the error string, producing "bad request: 404 Not Found" as the message — and the outer wrapper forces the status code to 400.


Requested Fix

Option Description
A — Pass through backend status codes When the backend returns a 4xx or 5xx, forward that status code to the client as-is. Only use proxy-generated error codes for proxy-internal failures.
B — Use semantically correct proxy error codes If the proxy must wrap backend errors, use 502 Bad Gateway for connection failures and 404 for backend-404 responses — not 400 for everything.
C — Include original status in structured error At minimum, include the original backend status as a field in the error JSON so clients can branch on it: {"statusCode": 400, "originalStatus": 404, "message": "..."}

Option A is strongly preferred. The proxy should be transparent to application-level HTTP semantics.

Note: The current behavior makes it impossible for SDK clients and browser applications to distinguish between "the resource doesn't exist" (404) and "your request was malformed" (400). This breaks standard REST error-handling patterns.


Environment

Daytona daemon v0.143.0-prod
Proxy *.proxy.daytona.works
Observed with opencode server (Go), Vite dev server (Node.js), Python HTTP server
SDK @opencode-ai/sdk, daytona-sdk (Python)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions