Last reviewed: 2026-10-20

Direct answer

Enforce a CometAPI response byte limit in Node.js by reading Response.body incrementally, counting each chunk’s byteLength, and rejecting the body before copying any chunk that would exceed your application limit. Decode and parse only after the stream finishes within that limit. Keep the same request deadline active through body consumption, and cancel the reader when the response is rejected.

Calling await response.json() and measuring the result afterward puts the check too late. The MDN Response.json() reference says the method reads the stream to completion and then parses it; it exposes no byte-limit parameter. A bounded reader gives your application a decision point before that full-body operation.

This tutorial implements a text-only, non-streaming Chat Completions client. The CometAPI chat reference documents the request route and the choices[].message response shape. The byte ceiling and timeout below are illustrative application settings, not CometAPI defaults. They protect the client’s response handling; they do not establish a billing limit or prove that upstream generation stopped.

Who this is for

Use this pattern in a server-side Node.js client that buffers one complete JSON response before using it. It is useful for tutorial backends, command-line checks, and application services where an unexpectedly large success or error body should fail predictably.

You need a current Node.js runtime with built-in Fetch and Web Streams. The helper was checked locally on Node.js 22.22.2 using synthetic responses. No authenticated CometAPI request or production load test was performed for this article. Keep your existing model capability checks and application schema validation around the helper.

Key takeaways

  • Count bytes delivered by the response stream before decoding them into a string.
  • Apply the same bound to successful and unsuccessful HTTP responses.
  • Keep one reader responsible for the body; avoid a parallel full-body diagnostic copy.
  • Cancel rejected bodies and release the reader lock on every completed read path.
  • Treat the byte ceiling, request deadline, output-token setting, and concurrent-request limit as separate controls.
  • Validate the parsed response before accepting a text result.

Build the bounded JSON reader

Save this module as limited-json.mjs. It uses a fixed-size application buffer, so accepted chunks cannot accumulate beyond the configured ceiling. An over-limit chunk is rejected before it is copied. The runtime may already have allocated that incoming chunk; this helper is not a cap on every transport allocation or on total process memory.

export class ResponseFault extends Error {
  constructor(code) {
    super(code);
    this.code = code;
  }
}

export async function readJsonWithinLimit(response, controller, maxBytes) {
  if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
    throw new ResponseFault('invalid_byte_limit');
  }
  if (!response.body) throw new ResponseFault('missing_body');

  const storage = new Uint8Array(maxBytes);
  const reader = response.body.getReader();
  let bytes = 0;
  try {
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      if (value.byteLength > maxBytes - bytes) {
        throw new ResponseFault('response_too_large');
      }
      storage.set(value, bytes);
      bytes += value.byteLength;
    }
    controller.signal.throwIfAborted();
    let text;
    try {
      text = new TextDecoder('utf-8', { fatal: true })
        .decode(storage.subarray(0, bytes));
    } catch {
      throw new ResponseFault('invalid_utf8');
    }
    let data;
    try {
      data = JSON.parse(text);
    } catch {
      throw new ResponseFault('invalid_json');
    }
    return { data, bytes };
  } catch (error) {
    controller.abort(error);
    await reader.cancel(error).catch(() => {});
    throw error;
  } finally {
    reader.releaseLock();
  }
}

The Node.js Web Streams reference documents the ownership rules behind this loop: getReader() locks the stream, read() returns the next chunk, cancel() cancels the underlying stream, and releaseLock() releases the reader. Releasing a lock alone is not the rejection action. The catch block aborts the request and attempts cancellation before releasing ownership.

The fixed buffer also avoids retaining a separate array of tiny chunks. Decoding happens once after the complete byte sequence arrives, so a multibyte UTF-8 character can cross chunk boundaries without being decoded in pieces. Fatal decoding rejects invalid UTF-8 instead of accepting replacement characters. JSON syntax errors become a fixed local code rather than an error message that could contain response content.

Connect it to a CometAPI request

Save this as chat-check.mjs beside the helper. Inject COMETAPI_KEY from your secret manager and set COMETAPI_MODEL to a model currently available for your account and this endpoint. Run it with node chat-check.mjs; no additional package is needed.

import { readJsonWithinLimit, ResponseFault } from './limited-json.mjs';

const key = process.env.COMETAPI_KEY;
const model = process.env.COMETAPI_MODEL;
if (!key || !model) throw new Error('Missing runtime configuration');

const maxBytes = 1024 * 1024;
const timeoutMs = 20000;
const controller = new AbortController();
const timer = setTimeout(() => {
  controller.abort(new ResponseFault('client_deadline'));
}, timeoutMs);
let status = null;

try {
  const response = await fetch('https://api.cometapi.com/v1/chat/completions', {
    method: 'POST',
    redirect: 'error',
    signal: controller.signal,
    headers: {
      authorization: `Bearer ${key}`,
      'content-type': 'application/json'
    },
    body: JSON.stringify({
      model,
      stream: false,
      messages: [{ role: 'user', content: 'Return one short greeting.' }]
    })
  });
  status = response.status;
  const mediaType = (response.headers.get('content-type') ?? '')
    .split(';')[0].trim().toLowerCase();
  if (!(mediaType === 'application/json' ||
        (mediaType.startsWith('application/') && mediaType.endsWith('+json')))) {
    controller.abort();
    throw new ResponseFault('unexpected_content_type');
  }

  const { data, bytes } = await readJsonWithinLimit(
    response, controller, maxBytes
  );
  if (!response.ok) throw new ResponseFault('http_error');
  if (!data || data.object !== 'chat.completion' ||
      !Array.isArray(data.choices)) {
    throw new ResponseFault('unexpected_envelope');
  }
  const choice = data.choices[0];
  if (typeof choice?.message?.content !== 'string' ||
      choice.message.refusal != null || choice.finish_reason !== 'stop') {
    throw new ResponseFault('unaccepted_text_result');
  }

  // Hand choice.message.content to the authorized application consumer.
  console.info(JSON.stringify({
    event: 'chat_response_accepted',
    http_status: status,
    response_bytes: bytes,
    byte_limit: maxBytes,
    text_present: true
  }));
} catch (error) {
  const reason = controller.signal.reason;
  const code = reason instanceof ResponseFault ? reason.code :
    error instanceof ResponseFault ? error.code : 'transport_error';
  console.error(JSON.stringify({
    event: 'chat_response_rejected',
    http_status: status,
    code
  }));
  process.exitCode = 1;
} finally {
  clearTimeout(timer);
}

The Node.js globals reference documents built-in fetch, AbortController, and throwIfAborted(). The timer remains active after headers arrive and while the helper reads the body. Clearing it immediately after fetch() resolves would leave body consumption outside that application deadline.

The acceptance check is deliberately specific to a plain text result. A tool call, refusal, length-limited completion, or another finish reason needs its own application branch. It should not become a successful greeting merely because the HTTP status is 200. Follow the existing response-object guide when extending the parser to another response family.

Contract details to verify

ControlWhat it measuresWhat to verify
Response-byte ceilingBytes exposed by Response.bodyRepresentative successful and error bodies fit the application policy.
Request deadlineTime allowed for the asynchronous request and body readA body that stalls after headers is canceled.
Output-token settingThe model’s supported generation allowanceCheck the selected model and current CometAPI reference before adding it.
Envelope validationThe response family and accepted text outcomeRefusals, tool calls, truncation, and unexpected shapes reach explicit branches.
Concurrency ceilingHow many requests allocate buffers togetherThe combined buffers, decoding, parsed objects, and runtime overhead fit capacity.

The CometAPI reference documents max_completion_tokens, including visible output and reasoning, but a token allowance does not define the byte size of a JSON envelope. Headers, metadata, error bodies, and the representation of text also matter. Choose your byte policy from observed response sizes and the application’s actual needs.

This counter intentionally measures the stream chunks, without trusting Content-Length as the enforcement mechanism. A declared length is not a substitute for counting what the reader receives. The helper accepts a complete body exactly at the ceiling and rejects the first chunk that would take it above the ceiling.

The deadline also has a practical limit: synchronous decoding and JSON.parse() run on the event loop, so the timer cannot interrupt them midway. A modest input ceiling reduces their permitted input, but it is not a CPU sandbox. For hostile or highly complex JSON, add structural validation and isolate expensive processing where your threat model requires it.

Verify the boundary locally

Start with synthetic responses so testing the failure path does not require credentials or paid traffic. This minimal exact-limit check can run in check-reader.mjs beside the helper:

import assert from 'node:assert/strict';
import { readJsonWithinLimit } from './limited-json.mjs';

const raw = new TextEncoder().encode('{"text":"你好"}');
const makeResponse = () => new Response(raw);
const accepted = await readJsonWithinLimit(
  makeResponse(), new AbortController(), raw.byteLength
);
assert.equal(accepted.data.text, '你好');
assert.equal(accepted.bytes, raw.byteLength);
await assert.rejects(
  readJsonWithinLimit(makeResponse(), new AbortController(), raw.byteLength - 1),
  { code: 'response_too_large' }
);
console.info('Byte boundary checks passed');

Expand the local matrix before integrating the helper:

FixtureRequired result
Valid JSON exactly at the ceilingAccept and report the actual byte count.
Valid JSON one byte above the ceilingReject as response_too_large and cancel the source.
UTF-8 character split across chunksDecode correctly after all accepted bytes arrive.
Invalid UTF-8Reject as invalid_utf8.
Small malformed JSONReject as invalid_json without including its contents in the error.
Headers arrive but the body stallsThe caller’s deadline aborts the read.
Oversized non-2xx JSON bodyEnforce the byte ceiling before parsing it.

Four synthetic helper checks passed during preparation: exact-limit JSON with split UTF-8, oversize cancellation of an open source, invalid JSON, and invalid UTF-8. The remaining integration cases above are reader verification steps, not reported test outcomes. Pair the stalled-body check with the site’s client-timeout workflow .

Failure modes

Checking after parsing. Measuring a string or object after response.json() cannot prevent that full read and parse. Move the policy into the body reader.

Counting characters. JavaScript string length does not measure the bytes received. Count each Uint8Array.byteLength before decoding, especially for multilingual output.

Creating a second consumer. A logging clone or diagnostic branch can retain another body outside this helper’s budget. Keep routine logs to HTTP status, byte count, configured limit, and fixed outcome codes.

Treating the buffer size as a process memory guarantee. Decoded strings, parsed objects, incoming chunks, transport state, and concurrent calls add memory. Bound concurrency and measure the complete application under representative load.

Retrying an oversize response automatically. Another identical request can repeat the same rejection and incur more work. Review the response contract, generation settings, and application ceiling before allowing a bounded retry. Local cancellation alone does not establish an upstream billing outcome.

Sources checked

These public technical references were fetched on 2026-10-20. They support an evergreen implementation task; this article does not report a new product launch.

FAQ

Is one mebibyte the correct limit for every client?

No. It is an illustrative value. Measure the response classes you accept, choose a ceiling with deliberate headroom, and account for peak concurrency. Keep that value in trusted application configuration.

Can this reader parse SSE responses?

No. The example explicitly sends stream: false and expects one JSON document. SSE requires event framing and separate limits for events, accumulated output, and stream lifetime.

Does a successful parse make the response safe to use?

No. Parsing establishes JSON syntax. The example then checks the chat envelope and a narrow text outcome. A business workflow still needs its own schema, authorization, and semantic checks.

Does aborting guarantee that generation or charging stopped?

This article makes no such guarantee. Aborting controls the local request path. Keep usage reconciliation and retry decisions separate from the response reader’s verdict.

Reader next step

Place the helper in one non-streaming client, run the synthetic boundary checks, and then test the complete deadline and error-body path against a controlled server. Record fixed outcome codes and byte counts without retaining prompts, keys, or response bodies. Once those checks pass, use a harmless authenticated request to verify the current CometAPI envelope through the same code path, and choose the production ceiling from that application’s measured needs.