HeyPuter/puter · error · HttpError
bad_request
bad_request
Error message
Missing `records` array
What it means
`POST /turn/ingest-usage` authenticates successfully but then requires a `records` array in the JSON body — each record describes a TURN egress usage entry to meter. If `records` is missing or not an array, the endpoint returns 400. Individual non-object entries are silently skipped, so only the top-level array shape is validated.
Source
Thrown at src/backend/controllers/peer/PeerController.ts:245
*/
#ingestUsage = async (req: Request, res: Response): Promise<void> => {
const cfg = this.config.peers;
if (!cfg || !cfg.internal_auth_secret) {
throw new HttpError(403, 'Forbidden', { legacyCode: 'forbidden' });
}
const expectedSecret = cfg.internal_auth_secret;
const header = req.headers['x-puter-internal-auth'];
if (
!expectedSecret ||
typeof header !== 'string' ||
!secretsEqual(header, expectedSecret)
) {
throw new HttpError(403, 'Forbidden', { legacyCode: 'forbidden' });
}
const { records } = req.body ?? {};
if (!Array.isArray(records)) {
throw new HttpError(400, 'Missing `records` array', {
legacyCode: 'bad_request',
});
}
for (const record of records) {
if (!record || typeof record !== 'object') continue;
const egressBytes = Number(record.egressBytes ?? 0);
if (egressBytes <= 0) continue;
const userUuid = record.userId
? base64urlToUuid(String(record.userId))
: null;
if (!userUuid) continue;
try {
const user = await this.stores.user.getByUuid(userUuid);
if (!user) continue;
const costInMicrocents =View on GitHub (pinned to 908ec23eda)
Solutions
- Ensure the body is `{ records: [...] }` where `records` is a JSON array.
- Each record should include `egressBytes` (positive number) and `userId` (base64url UUID).
- Set `Content-Type: application/json`.
- Validate the payload shape on the sender before posting.
Example fix
// before
await fetch('/turn/ingest-usage', {
method: 'POST',
body: JSON.stringify(record), // single object, not array
});
// after
await fetch('/turn/ingest-usage', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-puter-internal-auth': secret },
body: JSON.stringify({ records: [record] }),
}); Defensive patterns
Strategy: validation
Validate before calling
// Validate records shape before posting
if (!Array.isArray(records)) {
throw new Error('records must be an array');
}
await fetch('/turn/ingest-usage', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-puter-internal-auth': secret,
},
body: JSON.stringify({ records }),
}); Type guard
/** @param {unknown} v @returns {v is unknown[]} */
function isRecordArray(v) {
return Array.isArray(v);
} Prevention
- Always wrap records in `{ records: [...] }` — not a bare array or single object.
- Set `Content-Type: application/json`.
- Validate the payload shape on the sender before posting.
When it happens
Trigger: The ingestion service posts a body without `records`, or `records` is an object/number/string instead of an array. Each element should have `egressBytes` (number) and `userId` (base64url-encoded UUID).
Common situations: The ingestion service schema changed and stopped sending the array wrapper; a test fixture posts a single record object instead of `{ records: [...] }`; a serialization bug flattens the array.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/49d76a6e64555cd0.
Report an issue: GitHub.