{"record":{"id":"49d76a6e64555cd0","repo":"HeyPuter/puter","slug":"bad-request-49d76a","errorCode":"bad_request","errorMessage":"Missing `records` array","messagePattern":"Missing `records` array","errorType":"http","errorClass":"HttpError","httpStatus":400,"severity":"error","filePath":"src/backend/controllers/peer/PeerController.ts","lineNumber":245,"sourceCode":"     */\n    #ingestUsage = async (req: Request, res: Response): Promise<void> => {\n        const cfg = this.config.peers;\n        if (!cfg || !cfg.internal_auth_secret) {\n            throw new HttpError(403, 'Forbidden', { legacyCode: 'forbidden' });\n        }\n        const expectedSecret = cfg.internal_auth_secret;\n        const header = req.headers['x-puter-internal-auth'];\n        if (\n            !expectedSecret ||\n            typeof header !== 'string' ||\n            !secretsEqual(header, expectedSecret)\n        ) {\n            throw new HttpError(403, 'Forbidden', { legacyCode: 'forbidden' });\n        }\n\n        const { records } = req.body ?? {};\n        if (!Array.isArray(records)) {\n            throw new HttpError(400, 'Missing `records` array', {\n                legacyCode: 'bad_request',\n            });\n        }\n\n        for (const record of records) {\n            if (!record || typeof record !== 'object') continue;\n            const egressBytes = Number(record.egressBytes ?? 0);\n            if (egressBytes <= 0) continue;\n\n            const userUuid = record.userId\n                ? base64urlToUuid(String(record.userId))\n                : null;\n            if (!userUuid) continue;\n\n            try {\n                const user = await this.stores.user.getByUuid(userUuid);\n                if (!user) continue;\n                const costInMicrocents =","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/controllers/peer/PeerController.ts#L227-L263","documentation":"`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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nawait fetch('/turn/ingest-usage', {\n  method: 'POST',\n  body: JSON.stringify(record), // single object, not array\n});\n\n// after\nawait fetch('/turn/ingest-usage', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'x-puter-internal-auth': secret },\n  body: JSON.stringify({ records: [record] }),\n});","handlingStrategy":"validation","validationCode":"// Validate records shape before posting\nif (!Array.isArray(records)) {\n  throw new Error('records must be an array');\n}\nawait fetch('/turn/ingest-usage', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    'x-puter-internal-auth': secret,\n  },\n  body: JSON.stringify({ records }),\n});","typeGuard":"/** @param {unknown} v @returns {v is unknown[]} */\nfunction isRecordArray(v) {\n  return Array.isArray(v);\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["webrtc","turn","validation","internal-api","http-400","bad-request"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}