OpenHands / OpenHands/software-agent-sdk

Cloud-proxy (TS client) sends upstream body as object -> 422 from /api/cloud-proxy (Pydantic rejects non-string body)

Open Beginner friendly
#5,138 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug canvas-extensions javascript priority:medium
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

[!NOTE]
This issue was created by an AI agent (OpenHands) on behalf of @JuanMichelini during testing of enterprise PR OpenHands/enterprise#424 (/api/cloud-proxy route).

Summary

The TypeScript CloudClient places the upstream request body as a raw JS object into the cloud-proxy envelope's body field. The server-side CloudProxyRequest model (OpenHands/enterprise, PR #424) types body: str | None and forwards it verbatim (httpx content=...). A non-string body is therefore rejected by Pydantic at the proxy endpoint with HTTP 422, before any forwarding happens. The agent-canvas "High Risk / Continue" confirmation flow ({"accept": true}) reliably triggers this.

Reproduction

  1. In a saas-deploy preview running enterprise PR #424 (sha-58d7d3b), drive a conversation to a confirmation prompt.
  2. Click Continue on the "High Risk — Review carefully before proceeding. Do you want to continue with this action?" dialog.
  3. The POST to /api/cloud-proxy returns:
HTTP request failed (422 ):
{"detail":[{"type":"string_type","loc":["body","body"],"msg":"Input should be a valid string","input":{"accept":true}}]}

Backend log confirms the 422 is on the proxy endpoint itself (no forwarding attempted):

10.105.3.2:45524 - "POST /api/cloud-proxy HTTP/1.1" 422

Root cause

clients/typescript/src/client/cloud-client.ts, requestThroughProxy (currently around L695-L702):

body: {
  host: options.hostOverride,
  method: options.method,
  path: appendParams(options.path, options.params),
  headers: upstreamHeaders,
  body: options.body ?? null,   // <- raw object, e.g. { accept: true }
  ...(options.timeoutSeconds ? { timeout_seconds: options.timeoutSeconds } : {}),
},

fetchAndParse stringifies the outer envelope (L735: JSON.stringify(options.body)), but the inner upstream body is inserted as a raw object. The server expects body: str | None (verbatim text), so it 422s.

This is not a server-side bug: enterprise #424's CloudProxyRequest.body is intentionally str | None — text forwarded verbatim, with the caller supplying Content-Type (per the design comment and the tests, e.g. 'body': '{"foo":"bar"}' and "raw body forwarded verbatim, Content-Type comes from the caller"). The TS client is violating that contract.

(Aside: the envelope also sends a host field that the server ignores — Pydantic default extra='ignore' and the server derives the host server-side by design — so that's harmless.)

Suggested fix

In requestThroughProxy, stringify non-string upstream bodies and supply the upstream Content-Type for them, while leaving plain-string bodies untouched (caller controls Content-Type), preserving the cloud-proxy's verbatim-text contract:

const isJsonBody = options.body != null && typeof options.body !== 'string';
const upstreamHeaders = {
  ...this.buildUpstreamAuthHeaders(options),
  ...(this.orgId ? { 'X-Org-Id': this.orgId } : {}),
  ...(isJsonBody ? { 'Content-Type': 'application/json' } : {}),
  ...(options.headers ?? {}), // caller can still override
};
// ...
body: options.body == null
  ? null
  : typeof options.body === 'string'
    ? options.body
    : JSON.stringify(options.body),

Environment

  • enterprise-server image: ghcr.io/openhands/enterprise-server:sha-58d7d3b (enterprise PR #424)
  • TS client: clients/typescript/src/client/cloud-client.ts on main (L700 still body: options.body ?? null)
  • Shared staging litellm proxy + shared runtime-api (runtime.staging.all-hands.dev)
  • saas-deploy preview namespace (PR OpenHands/saas-deploy#1081)

Notes

  • This is distinct from the unrelated condense/"Unable to compute forgotten events" errors also seen on this preview, which were traced to the shared staging litellm guardrail requiring a system-first message.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in clients/typescript/src/client/cloud-client.ts at requestThroughProxy, then inspect fetchAndParse and the construction of the cloud-proxy envelope. Reproduce the confirmation request with an object body and verify that string bodies remain unchanged, object bodies satisfy the proxy contract, and caller-provided headers still take precedence.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.