






















1+import { createServer, type Server } from "node:http";
12import { describe, expect, it, vi } from "vitest";
23import { createClickClackClient } from "./http-client.js";
345+const LOOPBACK_RESPONSE_BYTES = 18 * 1024 * 1024;
6+7+async function listenLoopbackServer(server: Server): Promise<number> {
8+return await new Promise((resolve, reject) => {
9+server.once("error", reject);
10+server.listen(0, "127.0.0.1", () => {
11+server.off("error", reject);
12+const address = server.address();
13+if (!address || typeof address === "string") {
14+reject(new Error("expected loopback TCP address"));
15+return;
16+}
17+resolve(address.port);
18+});
19+});
20+}
21+22+function createOversizedJsonServer(): { server: Server; closed: Promise<number> } {
23+let resolveClosed: (sentBytes: number) => void = () => {};
24+const closed = new Promise<number>((resolve) => {
25+resolveClosed = resolve;
26+});
27+const server = createServer((req, res) => {
28+let sentBytes = 0;
29+let stopped = false;
30+let prefixSent = false;
31+const prefixChunk = Buffer.from('{"user":{"id":"');
32+const bodyChunk = Buffer.alloc(64 * 1024, 0x61);
33+const suffixChunk = Buffer.from('"}}');
34+const writeBuffer = (buffer: Buffer) => {
35+sentBytes += buffer.length;
36+if (!res.write(buffer)) {
37+res.once("drain", writeChunks);
38+return false;
39+}
40+return true;
41+};
42+const writeChunks = () => {
43+if (!prefixSent) {
44+prefixSent = true;
45+if (!writeBuffer(prefixChunk)) {
46+return;
47+}
48+}
49+while (true) {
50+if (stopped) {
51+return;
52+}
53+if (sentBytes + bodyChunk.length + suffixChunk.length >= LOOPBACK_RESPONSE_BYTES) {
54+break;
55+}
56+if (!writeBuffer(bodyChunk)) {
57+return;
58+}
59+}
60+if (!stopped) {
61+sentBytes += suffixChunk.length;
62+res.end(suffixChunk);
63+}
64+};
65+res.writeHead(200, { connection: "close", "content-type": "application/json" });
66+res.on("close", () => {
67+stopped = true;
68+resolveClosed(sentBytes);
69+});
70+req.on("aborted", () => {
71+stopped = true;
72+res.destroy();
73+});
74+writeChunks();
75+});
76+return { server, closed };
77+}
78+479function streamedErrorResponse(body: string, limit: number) {
580const encoded = new TextEncoder().encode(body);
681let readCount = 0;
@@ -39,6 +114,25 @@ function streamedErrorResponse(body: string, limit: number) {
39114}
4011541116describe("ClickClack HTTP client", () => {
117+it("bounds oversized success JSON responses and closes the stream early", async () => {
118+const { server, closed } = createOversizedJsonServer();
119+const port = await listenLoopbackServer(server);
120+const client = createClickClackClient({
121+baseUrl: `http://127.0.0.1:${port}`,
122+token: "test-token",
123+});
124+125+try {
126+await expect(client.me()).rejects.toThrow(
127+"ClickClack response: JSON response exceeds 16777216 bytes",
128+);
129+const sentBytes = await closed;
130+expect(sentBytes).toBeLessThan(LOOPBACK_RESPONSE_BYTES);
131+} finally {
132+server.close();
133+}
134+});
135+42136it("bounds error response bodies without using raw response.text()", async () => {
43137const streamed = streamedErrorResponse("x".repeat(9000), 8 * 1024);
44138const fetchMock = vi.fn(async () => streamed.response);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。