Skip to content

[Hackathon] A 200 response with a non-JSON body crashes with an opaque SyntaxError and breaks the --output json error envelope #92

Description

@Davidson3556

Discord Username / User ID

Davidson

What does this improvement do?

Make the HTTP layer handle a 200 OK response whose body is not JSON gracefully, with a typed error envelope, instead of crashing with an opaque SyntaxError (exit 1) that also breaks the --output json error contract.

Today, if the configured endpoint returns a 200 with a non-JSON body — a misconfigured TESTSPRITE_API_URL, a captive-portal / SSO login page, or a caching proxy that serves an HTML page with a 200 status — the CLI crashes with the raw response.json() parse error and no useful context.

Details / implementation notes

Reproduction (endpoint that returns 200 text/html):

$ node -e 'require("http").createServer((_,r)=>{r.writeHead(200,{"content-type":"text/html"});r.end("<!DOCTYPE html>...")}).listen(8791)' &
$ TESTSPRITE_API_KEY=sk-user-anything TESTSPRITE_API_URL=http://localhost:8791 \
    testsprite project list --output json
{
  "error": "Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON"
}
# exit 1

Two problems:

  1. Opaque error. The message is the raw V8 JSON-parse error. It never says the server returned a non-JSON response or hints that the endpoint may be pointing at the wrong host. There is no requestId for support to trace.
  2. Broken --output json error envelope. Every other CLI error emits the structured envelope {"error": {"code","message","nextAction","requestId","details"}}. This path emits a bare {"error": "<string>"}, so an agent that reads error.code gets undefined. That directly contradicts the contract the maintainers just hardened in fix(cli): emit JSON envelope for Commander parse errors under --output json #22 (JSON envelope for Commander parse errors) and fix(auth): preserve typed API error envelope when setup key verification fails #38 (preserve the typed error envelope).

Root cause. In src/lib/http.ts, the non-OK response path already reads the body defensively via safeReadJson() (which swallows non-abort parse errors and returns null). The OK path does a raw response.json():

if (response.ok) {
  ...
  return { body: (await response.json()) as T, requestId, status: response.status };
  //                    ^^^^^^^^^^^^^^^^^^^^ throws a raw SyntaxError on a non-JSON body
}

A non-abort parse failure there propagates uncaught to the top-level handler in index.ts, which falls through to the generic output.error(err.message) + exit(1) branch — hence the bare-string envelope.

Proposed fix. On the OK path, treat a non-abort JSON-parse failure the same way the rest of the CLI treats malformed responses: throw a typed ApiError (carrying the requestId) with a clear message — e.g. "the server returned a non-JSON response (HTTP 200); check that your endpoint points at the TestSprite API and not a proxy/login page." Abort/timeout errors mid-read keep being rethrown as today (the existing rethrowIfAbort call is preserved). This yields a proper exit code and a well-formed --output json envelope with a code/nextAction/requestId.

I have a PR ready for this.

Confirmations

  • I have searched existing issues and this is not a duplicate.
  • I have provided my Discord identity above for reward coordination.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions