fetch response body already used: preserve generation diagnostics once
·
A diagnostic logger can break a working generation client by reading the response body before the business handler does. The symptom often appears when middleware calls response.text() for logging and the caller later calls response.json(). Treat the response body as a stream with one owner. This article describes client behavior; Genace does not provide a special response buffering SDK.
Give one function ownership of parsing
Read text once, attempt JSON parsing from that string, and return a local envelope containing the HTTP status and parsed data. Keep any raw diagnostic excerpt bounded and private. A provider message, prompt fragment, or signed output URL may be sensitive even when the API key is absent.
export async function readEnvelope(response) {
const evidence = {
ok: response.ok,
status: response.status,
contentType: response.headers.get('content-type'),
};
let raw;
try {
raw = await response.text();
} catch {
return {
...evidence,
data: null,
readError: true,
parseError: false,
diagnosticLength: null,
};
}
let data = null;
let parseError = false;
try {
data = raw ? JSON.parse(raw) : null;
} catch {
parseError = true;
}
return {
...evidence,
data,
readError: false,
parseError,
diagnosticLength: raw.length,
};
}
The function deliberately records the diagnostic length instead of emitting raw content. Your application can store a reviewed, redacted excerpt in a restricted incident system when necessary. Do not log every response automatically just because buffering makes that convenient. Apply a response-size limit at the transport boundary for endpoints that can return unexpectedly large bodies.
Response.text() can reject after response headers have arrived: an aborted transfer, a locked or consumed body, or decoding failure is separate from malformed JSON. Save the HTTP evidence before reading. On readError, parsing was not attempted, so parseError is false and diagnosticLength is unknown, not a measured zero. Check readError before using data; response.ok describes the HTTP status, not successful body collection or generation acceptance. A null data value alone is ambiguous because an empty body or valid JSON null can also produce it.
The helper cannot recover bytes already consumed by another reader. It deliberately omits the thrown message, which can contain sensitive data. This wrapper receives an existing Response; fetch rejection before any Response exists needs a separate transport branch. It does not implement the size limit or a total timeout itself.
Interpret the envelope after consumption
For a readable successful generation POST, validate and preserve top-level job_id
before scheduling observation. For HTTP 502 with UPSTREAM_SUBMISSION_FAILED or
UPSTREAM_SUBMISSION_UNCERTAIN, preserve error.job_id when available. A subsequent
job snapshot uses id. If reading fails, this helper cannot reconstruct a lost
identifier. Retain an already known ID; never infer rejection, refund completion,
or permission to repeat a paid POST from a client body failure.
A successful job read is not necessarily a successful generation. Inspect state and allow outputs to be null. Parsing the body once makes these decisions consistent because the logger and business code see the same data object rather than competing readers. Retain the HTTP status separately so an HTML proxy error does not become an unexplained JSON exception.
Decide when cloning is appropriate
Fetch also exposes response cloning before consumption. A clone can be useful when two independent consumers are genuinely required, but it is not a free substitute for ownership. Stream branches can retain buffered data when consumers run at different speeds. Avoid cloning large media downloads solely to record a log, and do not clone an already consumed response.
For these small JSON endpoints, a single parser returning an immutable application object is generally easier to test. Keep media byte handling in a different path. The status endpoint supplies output URLs; it does not send the encoded video in the same JSON body.
Acceptance checks
Use a local Response fixture and verify the wrapper calls its body reader only once. Test valid accepted JSON, a failed job snapshot, an empty body, and HTML with HTTP 502. Confirm the original status survives every branch and that no parse error triggers another generation POST. Then test a logger that receives the parsed envelope instead of the original Response object.
Related implementation details are covered in error response validation, failed job responses, and structured observability.
Continue with code and integration
Where these facts come from
- Editorial keyword hypothesis; search volume not measured
- https://fetch.spec.whatwg.org/
- https://developer.mozilla.org/en-US/docs/Web/API/Response/text
- codebase: src/ai/api/errors.ts
- codebase: src/ai/jobs/http.ts