OpenHands / OpenHands/software-agent-sdk
Cloud-proxy (TS client) sends upstream body as object -> 422 from /api/cloud-proxy (Pydantic rejects non-string body)
Nobody has claimed this yet.
- 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-proxyroute).
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
- In a saas-deploy preview running enterprise PR #424 (
sha-58d7d3b), drive a conversation to a confirmation prompt. - Click Continue on the "High Risk — Review carefully before proceeding. Do you want to continue with this action?" dialog.
- The POST to
/api/cloud-proxyreturns:
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.tsonmain(L700 stillbody: 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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