




















@@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises";
55import path from "node:path";
66import process from "node:process";
77import { pathToFileURL } from "node:url";
8+import { readBoundedResponseText as readBoundedResponseTextWithLimit } from "../lib/bounded-response.mjs";
89910const DEFAULT_REGISTRY = "https://registry.npmjs.org";
1011const BULK_ADVISORY_PATH = "/-/npm/v1/security/advisories/bulk";
@@ -710,69 +711,35 @@ function resolveBulkAdvisoryResponseBodyMaxBytes() {
710711async function withBulkAdvisoryTimeout({ label, timeoutMs, run }) {
711712const controller = new AbortController();
712713let timeout;
714+const timeoutPromise = new Promise((_resolve, reject) => {
715+timeout = setTimeout(() => {
716+const error = new Error(`${label} exceeded timeout of ${timeoutMs}ms`);
717+controller.abort(error);
718+reject(error);
719+}, timeoutMs);
720+});
713721try {
714-return await Promise.race([
715-run(controller.signal),
716-new Promise((_resolve, reject) => {
717-timeout = setTimeout(() => {
718-const error = new Error(`${label} exceeded timeout of ${timeoutMs}ms`);
719-controller.abort(error);
720-reject(error);
721-}, timeoutMs);
722-}),
723-]);
722+return await Promise.race([run({ signal: controller.signal, timeoutPromise }), timeoutPromise]);
724723} finally {
725724if (timeout) {
726725clearTimeout(timeout);
727726}
728727}
729728}
730729731-async function readBoundedResponseText(response, maxBytes, label) {
732-const rawContentLength = response.headers?.get?.("content-length");
733-const contentLength =
734-rawContentLength && /^\d+$/u.test(rawContentLength) ? Number(rawContentLength) : undefined;
735-if (Number.isSafeInteger(contentLength) && contentLength > maxBytes) {
736-await response.body?.cancel().catch(() => undefined);
737-throw Object.assign(new Error(`${label} exceeded ${maxBytes} bytes`), { code: "ETOOBIG" });
738-}
739-740-if (!response.body) {
741-return "";
742-}
743-744-const reader = response.body.getReader();
745-const decoder = new TextDecoder();
746-const chunks = [];
747-let totalBytes = 0;
748-try {
749-for (;;) {
750-const { done, value } = await reader.read();
751-if (done) {
752-const tail = decoder.decode();
753-if (tail) {
754-chunks.push(tail);
755-}
756-break;
757-}
758-759-totalBytes += value.byteLength;
760-if (totalBytes > maxBytes) {
761-await reader.cancel().catch(() => undefined);
762-throw Object.assign(new Error(`${label} exceeded ${maxBytes} bytes`), { code: "ETOOBIG" });
763-}
764-chunks.push(decoder.decode(value, { stream: true }));
765-}
766-} finally {
767-reader.releaseLock();
768-}
769-770-return chunks.join("");
730+async function readBoundedResponseText(response, maxBytes, label, options = {}) {
731+return await readBoundedResponseTextWithLimit(response, label, maxBytes, {
732+signal: options.signal,
733+timeoutPromise: options.timeoutPromise,
734+formatTooLargeMessage: (messageLabel, bytes) => `${messageLabel} exceeded ${bytes} bytes`,
735+createTooLargeError: (message) => Object.assign(new Error(message), { code: "ETOOBIG" }),
736+});
771737}
772738773739export async function readBoundedBulkAdvisoryErrorText(
774740response,
775741maxChars = BULK_ADVISORY_ERROR_BODY_MAX_CHARS,
742+options = {},
776743) {
777744if (!response.body) {
778745return "";
@@ -782,10 +749,24 @@ export async function readBoundedBulkAdvisoryErrorText(
782749const decoder = new TextDecoder();
783750let text = "";
784751let truncated = false;
752+let canceled = false;
785753786754try {
787755while (text.length <= maxChars) {
788-const { done, value } = await reader.read();
756+const read = reader.read();
757+const readWithTimeout = options.timeoutPromise
758+ ? Promise.race([
759+read,
760+options.timeoutPromise.catch((error) => {
761+canceled = true;
762+void Promise.resolve()
763+.then(() => reader.cancel())
764+.catch(() => undefined);
765+throw error;
766+}),
767+])
768+ : read;
769+const { done, value } = await readWithTimeout;
789770if (done) {
790771text += decoder.decode();
791772break;
@@ -801,16 +782,21 @@ export async function readBoundedBulkAdvisoryErrorText(
801782} finally {
802783if (truncated) {
803784await reader.cancel().catch(() => undefined);
804-} else {
785+} else if (!canceled) {
805786reader.releaseLock();
806787}
807788}
808789809790return truncated ? `${text}\n[truncated]` : text;
810791}
811792812-async function readBulkAdvisoryJson(response, maxBytes) {
813-const text = await readBoundedResponseText(response, maxBytes, "Bulk advisory response body");
793+async function readBulkAdvisoryJson(response, maxBytes, options = {}) {
794+const text = await readBoundedResponseText(
795+response,
796+maxBytes,
797+"Bulk advisory response body",
798+options,
799+);
814800if (!text.trim()) {
815801throw new Error("Bulk advisory response body was empty");
816802}
@@ -828,7 +814,7 @@ export async function fetchBulkAdvisories({
828814return await withBulkAdvisoryTimeout({
829815label: "Bulk advisory request",
830816 timeoutMs,
831-run: async (signal) => {
817+run: async ({ signal, timeoutPromise }) => {
832818const response = await fetchImpl(url, {
833819method: "POST",
834820headers: {
@@ -840,13 +826,18 @@ export async function fetchBulkAdvisories({
840826});
841827842828if (!response.ok) {
843-const bodyText = await readBoundedBulkAdvisoryErrorText(response);
829+const bodyText = await readBoundedBulkAdvisoryErrorText(response, undefined, {
830+ timeoutPromise,
831+});
844832throw new Error(
845833`Bulk advisory request failed (${response.status} ${response.statusText}): ${bodyText}`,
846834);
847835}
848836849-return await readBulkAdvisoryJson(response, responseBodyMaxBytes);
837+return await readBulkAdvisoryJson(response, responseBodyMaxBytes, {
838+ signal,
839+ timeoutPromise,
840+});
850841},
851842});
852843}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。