-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Description
[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
- Run any HTTP server inside a sandbox on any port (e.g. port 4096)
- Make the server return
404for 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)
- Request that path through the preview proxy:
GET https://{port}-{token}.proxy.daytona.works/missing-resource - Observe response — client receives
400, not404:{"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) |