Guard dev health JSON parsing

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-06 20:17:47 -05:00
parent bfa60338cc
commit 9a8a169e95
7 changed files with 122 additions and 8 deletions

View file

@ -1,7 +1,12 @@
const DEFAULT_CAPTURED_OUTPUT_BYTES = 256 * 1024;
const DEFAULT_JSON_RESPONSE_BYTES = 64 * 1024;
function normalizeByteLimit(maxBytes) {
return Math.max(1, Math.trunc(maxBytes));
}
export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BYTES) {
const limit = Math.max(1, Math.trunc(maxBytes));
const limit = normalizeByteLimit(maxBytes);
const chunks = [];
let bufferedBytes = 0;
let totalBytes = 0;
@ -51,3 +56,38 @@ export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BY
},
};
}
export async function parseJsonResponseWithLimit(response, maxBytes = DEFAULT_JSON_RESPONSE_BYTES) {
const limit = normalizeByteLimit(maxBytes);
const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10);
if (Number.isFinite(contentLength) && contentLength > limit) {
throw new Error(`Response exceeds ${limit} bytes`);
}
if (!response.body) {
return JSON.parse("");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let text = "";
let totalBytes = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
if (totalBytes > limit) {
await reader.cancel("response too large");
throw new Error(`Response exceeds ${limit} bytes`);
}
text += decoder.decode(value, { stream: true });
}
text += decoder.decode();
} finally {
reader.releaseLock();
}
return JSON.parse(text);
}

View file

@ -1,4 +1,5 @@
const DEFAULT_CAPTURED_OUTPUT_BYTES = 256 * 1024;
const DEFAULT_JSON_RESPONSE_BYTES = 64 * 1024;
export type CapturedOutput = {
text: string;
@ -6,8 +7,12 @@ export type CapturedOutput = {
totalBytes: number;
};
function normalizeByteLimit(maxBytes: number) {
return Math.max(1, Math.trunc(maxBytes));
}
export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BYTES) {
const limit = Math.max(1, Math.trunc(maxBytes));
const limit = normalizeByteLimit(maxBytes);
const chunks: Buffer[] = [];
let bufferedBytes = 0;
let totalBytes = 0;
@ -57,3 +62,41 @@ export function createCapturedOutputBuffer(maxBytes = DEFAULT_CAPTURED_OUTPUT_BY
},
};
}
export async function parseJsonResponseWithLimit<T>(
response: Response,
maxBytes = DEFAULT_JSON_RESPONSE_BYTES,
): Promise<T> {
const limit = normalizeByteLimit(maxBytes);
const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10);
if (Number.isFinite(contentLength) && contentLength > limit) {
throw new Error(`Response exceeds ${limit} bytes`);
}
if (!response.body) {
return JSON.parse("") as T;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let text = "";
let totalBytes = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
if (totalBytes > limit) {
await reader.cancel("response too large");
throw new Error(`Response exceeds ${limit} bytes`);
}
text += decoder.decode(value, { stream: true });
}
text += decoder.decode();
} finally {
reader.releaseLock();
}
return JSON.parse(text) as T;
}

View file

@ -5,7 +5,7 @@ import path from "node:path";
import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { fileURLToPath } from "node:url";
import { createCapturedOutputBuffer } from "./dev-runner-output.mjs";
import { createCapturedOutputBuffer, parseJsonResponseWithLimit } from "./dev-runner-output.mjs";
import { shouldTrackDevServerPath } from "./dev-runner-paths.mjs";
const mode = process.argv[2] === "watch" ? "watch" : "dev";
@ -430,7 +430,7 @@ async function getDevHealthPayload() {
if (!response.ok) {
throw new Error(`Health request failed (${response.status})`);
}
return await response.json();
return await parseJsonResponseWithLimit(response);
}
async function waitForChildExit() {

View file

@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } f
import path from "node:path";
import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { createCapturedOutputBuffer } from "./dev-runner-output.mjs";
import { createCapturedOutputBuffer, parseJsonResponseWithLimit } from "./dev-runner-output.mjs";
import { shouldTrackDevServerPath } from "./dev-runner-paths.mjs";
import { createDevServiceIdentity, repoRoot } from "./dev-service-profile.ts";
import {
@ -487,7 +487,7 @@ async function getDevHealthPayload() {
if (!response.ok) {
throw new Error(`Health request failed (${response.status})`);
}
return await response.json();
return await parseJsonResponseWithLimit<{ devServer?: { enabled?: boolean; autoRestartEnabled?: boolean; activeRunCount?: number } }>(response);
}
async function waitForChildExit() {