Skip to content

[Hackathon] Bound non-2xx JSON response bodies before parsing API errors #528

Closed as not planned

Description

@ayush-that

Verified against (commit SHA or released version)

Published @testsprite/testsprite-cli@0.14.0 and source commit 988c0355bb05631e9c6c1785e443937ff2a88c0e. macOS 27.2 (arm64), Node.js v26.12.0.

Discord Username / User ID

shydev69

What does this improvement do?

The HTTP client bounds successful JSON responses but parses non-2xx bodies with an unbounded response.json(). Add a bounded error-body read so an unexpectedly large error does not require buffering and parsing its entire body. This extends the current success-only limit; it is not a claim that the documented option already covers errors.

Details / implementation notes

Steps to reproduce

The injected fetch returns an 8,350-byte response while maxResponseBytes is 1,024. It checks HTTP 200 and HTTP 400, with and without Content-Length. No socket is opened.

From a checkout of the verified commit:

npm ci
npm run build

Save the script below as error-body.mjs, then run:

node error-body.mjs ./dist
error-body.mjs
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

const dist = resolve(process.argv[2] ?? './dist');
const { HttpClient } = await import(pathToFileURL(resolve(dist, 'lib/http.js')));
const body = JSON.stringify({ error: {
  code: 'VALIDATION_ERROR', message: 'Synthetic validation response',
  nextAction: 'No action', requestId: 'local-cap-check',
  details: { sample: 'x'.repeat(8192) },
} });
for (const status of [200, 400]) {
  for (const declaredLength of [true, false]) {
    const client = new HttpClient({
      baseUrl: 'https://unused.invalid/api/cli/v1', apiKey: 'sk-dummy-cap-check',
      maxResponseBytes: 1024,
      fetchImpl: async () => new Response(body, { status, headers: {
        'content-type': 'application/json',
        ...(declaredLength ? { 'content-length': String(Buffer.byteLength(body)) } : {}),
      } }),
    });
    try {
      await client.get('/synthetic', { retry: false });
      console.log(JSON.stringify({ status, declaredLength, result: 'unexpected success' }));
    } catch (error) {
      console.log(JSON.stringify({ status, declaredLength,
        responseBytes: Buffer.byteLength(body), capBytes: 1024,
        errorCode: error.code, retainedSampleLength: error.details?.sample?.length ?? 0,
      }));
    }
  }
}

Actual result

The result is the same with and without Content-Length:

HTTP status Error code Retained sample length
200 PAYLOAD_TOO_LARGE 0
400 VALIDATION_ERROR 8192

The error path parses and retains content beyond the configured success-body budget. Expected: a separately defined error-body budget prevents unbounded consumption and returns a useful typed error.

Proposed fix

Use a bounded reader for non-2xx JSON responses, with the cap and truncation/error behavior agreed during triage. Preserve abort, interruption, timeout, and normal API-error handling. Check both declared and streamed lengths.

Relevant code: non-2xx read, safeReadJson, and the success-only option contract.

Related: PR #281 deliberately bounds successful responses only. This proposal does not include the docs-upload diagnostic prefix already covered by PR #409.

Confirmations

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

Activity

  1. fyZhang66 commented on Oct 22, 2026

    @fyZhang66
    Contributor

    Thanks @ayush-that. Error bodies come only from our API and are small envelopes, so an oversized error response doesn't happen in practice; the cap is deliberately on success bodies. Closing as not planned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    hackathonCLI hackathon submissions

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions