fix(http): surface the server's error body on a failed tool call
Closed
ED
1mo ago+164+164+012345678901234567890123456789-4-4-0123456789
Problem
When an HTTP call fails with a non-2xx status, the http protocols discard the server's response body — where servers put the actual reason ({ "error": "..." }) — and surface only the status code.
HttpCommunicationProtocol.callTool(axios) re-threw the rawAxiosError, whose.messageis the generic"Request failed with status code 403". The body sits onerror.response.dataand never reaches.message. It's also dropped entirely when the error crosses a serialization boundary (e.g.JSON.stringifyinside anisolated-vmtool runner like@utcp/code-mode), becauseError.messageis non-enumerable andAxiosErrordoesn't serialize itsresponse. In a code-mode agent a server's 403/400 reaches the model as{}.StreamableHttpCommunicationProtocol/SseCommunicationProtocol(fetch) threwHTTP ${status}: ${statusText}during manual discovery (registerManual) without ever reading the body, so a refused discovery surfaced as"HTTP 403: Forbidden"inerrors[].
Fix
callToolnormalizes a failed HTTP call into anErrorthat folds the status + server body into.messageAND attaches enumerablestatus/datafields, so the reason survives both.messagereaders and structured serialization. Non-HTTP errors (network, timeout) pass through unchanged.- streamable_http + sse read the response body before throwing during discovery and fold it into the message (falling back to
statusTextwhen empty). TheircallToolpaths are stubs (no HTTP call yet), so discovery is the only real failure surface today.
No public signature changes.
Tests
- A
/forbiddenroute (403 + JSON body) + acallTooltest asserting the thrown error carries the body in.message, exposesstatus/data, and round-trips throughJSON.stringify(the sandbox case). - A
GET /forbidden-discoveryroute (403 + text body) + tests asserting both streamable and sseregisterManualsurface the body inerrors[].
bun test packages/http/tests/ → 85 pass, 0 fail. build:http clean (DTS included) once core is built first.