




















11const DEFAULT_ERROR_BODY_MAX_BYTES = 8 * 1024;
22const DEFAULT_ERROR_BODY_MAX_CHARS = 1_000;
3+const DEFAULT_JSON_BODY_MAX_BYTES = 64 * 1024 * 1024;
34const TRUNCATED_SUFFIX = "... [truncated]";
4556type ResponseTextSnippetOptions = {
67maxBytes?: number;
78maxChars?: number;
89};
91011+type ResponseJsonOptions = {
12+maxBytes?: number;
13+errorPrefix: string;
14+};
15+1016type ResponsePrefix = {
1117bytes: Uint8Array[];
1218length: number;
@@ -35,6 +41,26 @@ export async function readResponseTextSnippet(
3541return collapsed;
3642}
374344+export async function readResponseJsonWithLimit(
45+res: Response,
46+options: ResponseJsonOptions,
47+): Promise<unknown> {
48+const maxBytes = options.maxBytes ?? DEFAULT_JSON_BODY_MAX_BYTES;
49+const contentLength = parseContentLength(res.headers.get("content-length"), options.errorPrefix);
50+if (typeof contentLength === "number" && contentLength > maxBytes) {
51+await cancelResponseBody(res);
52+throw responseTooLarge(options.errorPrefix, contentLength, maxBytes);
53+}
54+55+const text = await readResponseTextWithLimit(res, maxBytes, options.errorPrefix);
56+57+try {
58+return JSON.parse(text);
59+} catch (cause) {
60+throw new Error(`${options.errorPrefix}: malformed JSON response`, { cause });
61+}
62+}
63+3864async function readResponsePrefix(res: Response, maxBytes: number): Promise<ResponsePrefix> {
3965const body = res.body;
4066if (!body || typeof body.getReader !== "function") {
@@ -79,6 +105,79 @@ async function readResponsePrefix(res: Response, maxBytes: number): Promise<Resp
79105return { bytes: chunks, length, truncated };
80106}
81107108+async function readResponseTextWithLimit(
109+res: Response,
110+maxBytes: number,
111+errorPrefix: string,
112+): Promise<string> {
113+const body = res.body;
114+if (!body || typeof body.getReader !== "function") {
115+return "";
116+}
117+118+const reader = body.getReader();
119+const chunks: Uint8Array[] = [];
120+let length = 0;
121+122+try {
123+while (true) {
124+const { done, value } = await reader.read();
125+if (done) {
126+break;
127+}
128+if (!value?.length) {
129+continue;
130+}
131+132+const nextLength = length + value.length;
133+if (nextLength > maxBytes) {
134+await reader.cancel().catch(() => undefined);
135+throw responseTooLarge(errorPrefix, nextLength, maxBytes);
136+}
137+138+chunks.push(value);
139+length = nextLength;
140+}
141+} finally {
142+try {
143+reader.releaseLock();
144+} catch {}
145+}
146+147+return new TextDecoder().decode(joinChunks(chunks, length));
148+}
149+150+async function cancelResponseBody(res: Response): Promise<void> {
151+const body = res.body;
152+if (!body || typeof body.cancel !== "function") {
153+return;
154+}
155+await body.cancel().catch(() => undefined);
156+}
157+158+function parseContentLength(raw: string | null, errorPrefix: string): number | undefined {
159+const trimmed = raw?.trim();
160+if (!trimmed) {
161+return undefined;
162+}
163+if (!/^(0|[1-9]\d*)$/.test(trimmed)) {
164+throw new Error(`${errorPrefix}: invalid content-length header: ${raw}`);
165+}
166+const value = Number(trimmed);
167+if (!Number.isSafeInteger(value)) {
168+throw new Error(`${errorPrefix}: invalid content-length header: ${raw}`);
169+}
170+return value;
171+}
172+173+function responseTooLarge(errorPrefix: string, size: number, maxBytes: number): Error {
174+return new Error(responseTooLargeMessage(errorPrefix, size, maxBytes));
175+}
176+177+function responseTooLargeMessage(errorPrefix: string, size: number, maxBytes: number): string {
178+return `${errorPrefix}: response body too large: ${size} bytes (limit: ${maxBytes} bytes)`;
179+}
180+82181function joinChunks(chunks: Uint8Array[], length: number): Uint8Array {
83182if (chunks.length === 1 && chunks[0]?.length === length) {
84183return chunks[0];
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。