/** * Decode a response body honoring the declared charset (Content-Type header, * then, for HTML or untyped bodies, a cheap sniff), falling * back to UTF-8. */ export function decodeBody(bytes: Buffer, contentTypeHeader: string): string { let label = /charset\s*=\s*"?([\w-]+)"?/i.exec(contentTypeHeader)?.[1]; // A JSON or text payload may quote a `` tag as data; only an // HTML document's own markup declares its encoding that way. if (!label && (contentTypeHeader.trim() === "" || /html/i.test(contentTypeHeader))) { // All charsets we can decode are ASCII-compatible in the prefix, so a // latin1 view of the first 2KB is enough to find a . label = /]+charset\s*=\s*["']?([\w-]+)/i.exec(bytes.subarray(0, 2048).toString("latin1"))?.[1]; } if (label && !/^utf-?8$/i.test(label)) { try { // Bun.Encoding's union is narrower than the runtime, which accepts // WHATWG labels (shift_jis, euc-kr, gbk, big5, …); unknowns throw here. return new TextDecoder(label as Bun.Encoding).decode(bytes); } catch { // Unknown/unsupported label — fall back to UTF-8. } } // TextDecoder drops a leading BOM, which JSON.parse would otherwise reject. return new TextDecoder().decode(bytes); } /** * Read a response body up to `maxBytes`, stopping once the cap is crossed. * `bytes` never exceeds the cap; `truncated` reports whether the body was cut * mid-stream. Returns `null` when the response carries no readable body. */ export async function readBodyCapped( response: Response, maxBytes: number, ): Promise<{ bytes: Buffer; truncated: boolean } | null> { const reader = response.body?.getReader(); if (!reader) return null; // One buffer grown up to the cap: a body delivered as many tiny (or empty) // chunks must not retain one object per chunk. let buffer = new Uint8Array(Math.min(maxBytes, 64 * 1024)); let size = 0; let truncated = false; try { for (;;) { const { done, value } = await reader.read(); if (done) break; const accepted = Math.min(value.byteLength, maxBytes - size); if (size + accepted > buffer.byteLength) { const grown = new Uint8Array(Math.min(maxBytes, Math.max(size + accepted, buffer.byteLength * 2))); grown.set(buffer.subarray(0, size)); buffer = grown; } buffer.set(value.subarray(0, accepted), size); size += accepted; if (accepted < value.byteLength) { // Crossing chunk: keep the head up to the cap, mark the body cut and // stop reading so an oversized response cannot balloon memory. truncated = true; await reader.cancel().catch(() => undefined); break; } } } finally { reader.releaseLock(); } return { bytes: Buffer.from(buffer.buffer, buffer.byteOffset, size), truncated }; }