mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix: complete debug-log request bounds
This commit is contained in:
1 parent
a948274326
commit
dd627891f8
3 files changed
+40
-7
No files matched your search
@@ -17,21 +17,29 @@ async function readRequestBodyCapped(req, maxBytes) {
|
|||||||
function readRequestBody(req, maxBytes = 1024 * 1024) {
|
function readRequestBody(req, maxBytes = 1024 * 1024) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
let bodyTooLarge = false;
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
req.on('data', (chunk) => {
|
req.on('data', (chunk) => {
|
||||||
|
// Keep draining after the cap without retaining more bytes. Destroying
|
||||||
|
// the request also destroys its socket, so the handler cannot deliver
|
||||||
|
// the fixed error response it constructs for BODY_TOO_LARGE.
|
||||||
|
if (bodyTooLarge) return;
|
||||||
total += chunk.length;
|
total += chunk.length;
|
||||||
if (total > maxBytes) {
|
if (total > maxBytes) {
|
||||||
// Same marker readRequestBodyCapped sets, so a caller can tell an
|
// Same marker readRequestBodyCapped sets, so a caller can tell an
|
||||||
// oversized body from a genuine failure without matching on text.
|
// oversized body from a genuine failure without matching on text.
|
||||||
|
bodyTooLarge = true;
|
||||||
|
chunks.length = 0;
|
||||||
const error = new Error(`Request body exceeds ${maxBytes} bytes`);
|
const error = new Error(`Request body exceeds ${maxBytes} bytes`);
|
||||||
error.code = 'BODY_TOO_LARGE';
|
error.code = 'BODY_TOO_LARGE';
|
||||||
reject(error);
|
reject(error);
|
||||||
req.destroy();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
});
|
});
|
||||||
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
req.on('end', () => {
|
||||||
|
if (!bodyTooLarge) resolve(Buffer.concat(chunks).toString('utf8'));
|
||||||
|
});
|
||||||
req.on('error', reject);
|
req.on('error', reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ function createDebugLogHandler({ sourceRoot = defaultSourceRoot } = {}) {
|
|||||||
() => 0,
|
() => 0,
|
||||||
);
|
);
|
||||||
if (size + Buffer.byteLength(line) > REALTIME_DEBUG_LOG_MAX_FILE_BYTES) {
|
if (size + Buffer.byteLength(line) > REALTIME_DEBUG_LOG_MAX_FILE_BYTES) {
|
||||||
|
// Node does not replace an existing destination on Windows. Remove the
|
||||||
|
// retained generation explicitly so every rotation behaves the same on
|
||||||
|
// supported platforms rather than failing after the first 64 MB.
|
||||||
|
await fsp.rm(`${logFile}.1`, { force: true });
|
||||||
await fsp.rename(logFile, `${logFile}.1`);
|
await fsp.rename(logFile, `${logFile}.1`);
|
||||||
}
|
}
|
||||||
await fsp.appendFile(logFile, line);
|
await fsp.appendFile(logFile, line);
|
||||||
@@ -67,7 +71,7 @@ function createDebugLogHandler({ sourceRoot = defaultSourceRoot } = {}) {
|
|||||||
if (!allow(clientKey(req))) {
|
if (!allow(clientKey(req))) {
|
||||||
res.writeHead(429, {
|
res.writeHead(429, {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Retry-After': '5',
|
'Retry-After': '60',
|
||||||
});
|
});
|
||||||
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
|
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import { createServer } from 'node:http';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import {
|
import {
|
||||||
mkdtempSync,
|
mkdtempSync,
|
||||||
@@ -365,7 +366,7 @@ test('the debug-log sink stays bounded, rate limited, and quiet about failures',
|
|||||||
else if (response.status === 204) accepted += 1;
|
else if (response.status === 204) accepted += 1;
|
||||||
}
|
}
|
||||||
assert.ok(limited, 'the sink refuses a caller past its per-minute ceiling');
|
assert.ok(limited, 'the sink refuses a caller past its per-minute ceiling');
|
||||||
assert.equal(limited.headers['retry-after'], '5');
|
assert.equal(limited.headers['retry-after'], '60');
|
||||||
assert.deepEqual(limited.json(), { error: 'Rate limit exceeded' });
|
assert.deepEqual(limited.json(), { error: 'Rate limit exceeded' });
|
||||||
// The limiter counts requests rather than successful writes, so the malformed
|
// The limiter counts requests rather than successful writes, so the malformed
|
||||||
// record above already spent one of the 120 slots.
|
// record above already spent one of the 120 slots.
|
||||||
@@ -379,6 +380,24 @@ test('the debug-log sink stays bounded, rate limited, and quiet about failures',
|
|||||||
assert.ok(lines.every((line) => JSON.parse(line).loggedAt));
|
assert.ok(lines.every((line) => JSON.parse(line).loggedAt));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an oversized debug-log request receives the fixed error response', async (t) => {
|
||||||
|
const handler = install(openAiRealtimeProxy({ sourceRoot: root(t) })).get(
|
||||||
|
'/api/realtime/debug-log',
|
||||||
|
);
|
||||||
|
const server = createServer(handler);
|
||||||
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
t.after(() => new Promise((resolve) => server.close(resolve)));
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`http://127.0.0.1:${server.address().port}/api/realtime/debug-log`,
|
||||||
|
{ method: 'POST', body: 'x'.repeat(8 * 1024 * 1024 + 1) },
|
||||||
|
);
|
||||||
|
assert.equal(response.status, 400);
|
||||||
|
assert.deepEqual(await response.json(), {
|
||||||
|
error: 'Failed to write Realtime debug log',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('the debug log rotates instead of growing without bound', async (t) => {
|
test('the debug log rotates instead of growing without bound', async (t) => {
|
||||||
const sourceRoot = root(t);
|
const sourceRoot = root(t);
|
||||||
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
|
const handler = install(openAiRealtimeProxy({ sourceRoot })).get(
|
||||||
@@ -390,7 +409,9 @@ test('the debug log rotates instead of growing without bound', async (t) => {
|
|||||||
// single page could grow it for as long as the dev server ran. Each record
|
// single page could grow it for as long as the dev server ran. Each record
|
||||||
// here is ~1 MB, well inside the body cap.
|
// here is ~1 MB, well inside the body cap.
|
||||||
const pad = 'x'.repeat(1024 * 1024);
|
const pad = 'x'.repeat(1024 * 1024);
|
||||||
for (let n = 0; n < 40; n += 1) {
|
// Seventy-two records force two rotations. The second one replaces an
|
||||||
|
// existing `.1`, which requires an explicit removal on Windows.
|
||||||
|
for (let n = 0; n < 72; n += 1) {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(
|
(
|
||||||
await request(handler, {
|
await request(handler, {
|
||||||
@@ -409,7 +430,7 @@ test('the debug log rotates instead of growing without bound', async (t) => {
|
|||||||
live + previous <= 64 * 1024 * 1024,
|
live + previous <= 64 * 1024 * 1024,
|
||||||
'both generations together stay within twice the ceiling',
|
'both generations together stay within twice the ceiling',
|
||||||
);
|
);
|
||||||
// Unrotated, these records would be ~42 MB in one file.
|
// Unrotated, these records would be ~75 MB in one file.
|
||||||
assert.ok(live + previous < 42 * 1024 * 1024);
|
assert.ok(live + previous < 64 * 1024 * 1024);
|
||||||
assert.ok(!existsSync(`${file}.2`), 'exactly one generation is retained');
|
assert.ok(!existsSync(`${file}.2`), 'exactly one generation is retained');
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user