Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
39df200ac9 [autofix.ci] apply automated fixes 2025-08-05 19:08:43 +00:00
Claude Bot
7822083657 fix: resolve hanging issue with multiple HTTP request writes (#21620)
This fix addresses a race condition in the HTTP client where multiple
write() calls followed by end() would cause the request to hang.

The issue occurred when:
1. First write() call adds chunk to buffer
2. Second write() call triggers streaming mode (startFetch)
3. end() is called before the async generator sets up promise handlers
4. resolveNextChunk callback would be undefined when called

The fix ensures proper async coordination by:
- Using process.nextTick() to defer promise resolution
- Capturing the resolve function reference before clearing resolveNextChunk
- This prevents race conditions between chunk writes and stream ending

Tests added to verify the fix works for both HTTP and HTTPS requests.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-05 19:06:15 +00:00
3 changed files with 342 additions and 6 deletions

View File

@@ -176,7 +176,9 @@ function ClientRequest(input, options, cb) {
if (!this.finished) {
send();
resolveNextChunk?.(true);
process.nextTick(() => {
resolveNextChunk?.(true);
});
}
return this;
@@ -339,12 +341,15 @@ function ClientRequest(input, options, cb) {
while (!self.finished) {
yield await new Promise(resolve => {
resolveNextChunk = end => {
const currentResolve = resolve;
resolveNextChunk = undefined;
if (end) {
resolve(undefined);
} else {
resolve(self[kBodyChunks].shift());
}
process.nextTick(() => {
if (end) {
currentResolve(undefined);
} else {
currentResolve(self[kBodyChunks].shift());
}
});
};
});

View File

@@ -3078,6 +3078,155 @@ test("should handle invalid method", async () => {
await promise;
});
test("multiple request writes should not hang (issue #21620)", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
let responseReceived = false;
await using server = http.createServer((req, res) => {
let body = "";
req.on("data", chunk => {
body += chunk.toString();
});
req.on("end", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ receivedBody: body }));
});
});
server.listen(0);
await once(server, "listening");
const address = server.address() as AddressInfo;
const jsonStr = JSON.stringify({ key: "val", key2: 200 });
const req = http.request(
{
hostname: "localhost",
port: address.port,
method: "POST",
headers: {
"content-type": "application/json",
},
},
res => {
let data = "";
res.on("data", chunk => {
data += chunk.toString();
});
res.on("end", () => {
try {
const response = JSON.parse(data);
expect(response.receivedBody).toBe(jsonStr);
responseReceived = true;
resolve();
} catch (err) {
reject(err);
}
});
},
);
req.on("error", reject);
// Add timeout to prevent hanging
const timeout = setTimeout(() => {
if (!responseReceived) {
req.destroy();
reject(new Error("Request timed out - this indicates the hanging bug"));
}
}, 1000);
// Multiple writes cause the issue
req.write(jsonStr.slice(0, 5));
setTimeout(() => {
req.write(jsonStr.slice(5));
req.end();
}, 100);
try {
await promise;
clearTimeout(timeout);
} catch (err) {
clearTimeout(timeout);
throw err;
}
});
test("multiple https request writes should not hang", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
let responseReceived = false;
await using server = https.createServer(COMMON_TLS_CERT, (req, res) => {
let body = "";
req.on("data", chunk => {
body += chunk.toString();
});
req.on("end", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ receivedBody: body }));
});
});
server.listen(0);
await once(server, "listening");
const address = server.address() as AddressInfo;
const jsonStr = JSON.stringify({ key: "val", key2: 200 });
const req = https.request(
{
hostname: "localhost",
port: address.port,
method: "POST",
headers: {
"content-type": "application/json",
},
rejectUnauthorized: false, // For test cert
},
res => {
let data = "";
res.on("data", chunk => {
data += chunk.toString();
});
res.on("end", () => {
try {
const response = JSON.parse(data);
expect(response.receivedBody).toBe(jsonStr);
responseReceived = true;
resolve();
} catch (err) {
reject(err);
}
});
},
);
req.on("error", reject);
// Add timeout to prevent hanging
const timeout = setTimeout(() => {
if (!responseReceived) {
req.destroy();
reject(new Error("HTTPS request timed out - this indicates the hanging bug"));
}
}, 1000);
// Multiple writes cause the issue
req.write(jsonStr.slice(0, 5));
setTimeout(() => {
req.write(jsonStr.slice(5));
req.end();
}, 100);
try {
await promise;
clearTimeout(timeout);
} catch (err) {
clearTimeout(timeout);
throw err;
}
});
describe("HTTP Server Security Tests - Advanced", () => {
// Setup and teardown utilities
let server;

View File

@@ -0,0 +1,182 @@
/**
* Regression test for issue #21620 - Multiple data writes on outgoing http/https request cause the connection to hang
* @see https://github.com/oven-sh/bun/issues/21620
*/
import { expect, test } from "bun:test";
import { once } from "node:events";
import http from "node:http";
import https from "node:https";
import type { AddressInfo } from "node:net";
const COMMON_TLS_CERT = {
cert: `-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJAKKkAbUUoCAuMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMMCWxv
Y2FsaG9zdDAeFw0yMzA4MDkxODU4NTlaFw0yNDA4MDgxODU4NTlaMBQxEjAQBgNV
BAMMCWxvY2FsaG9zdDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC7Ut9X5Hs3o/d3
RQznTaEEvw1tZnDww4RJRgkAPsK6HoAo+SxsPrCRQ1M/3S7Sc7YNjkOT6K/OP9lk
yVxRcN29AgMBAAEwDQYJKoZIhvcNAQELBQADQQBAFqJMRdVFNWYMnSRj8xXYYvHI
KV5yqSYhcmPF6BvFzWUPJlxbhZZU4I3KWqkT2qKrOyKwlsrpVECNxTUmjJX8
-----END CERTIFICATE-----`,
key: `-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBALtS31fkezej93dFDOdNoQS/DW1mcPDDhElGCQA+wroegCj5LGw+
sJFDUz/dLtJztg2OQ5Por84/2WTJXFFw3b0CAwEAAQJBAK5cXmHfCaYJTwJKpqHi
NZIb4HOw3l8JLT6V8lJoJjkUyQeRfHRoqMTBNV7HGVr8HXeJF6mHYVzXhh7CKKBn
lCECIQDcOTGFE7gU6zW8bV2b1JzG1Hv5jJiO2xON9U3qxnKNZwIhAOKGOLQcHrOO
hGdJWp5YTJD5K3vxrW6HzfN/Lr8yTpGVAiEA4mD8YjQxuCJ3yDY1zR5U2n7rE5ON
LYZa5GGl9g5w6YMCIQDH4U+7K3mw7yV3U6gHfLaV0+6nOW9l1lCY2vXzjUr5HQIg
YMU+J1Y5SBo9PHLLqJQ9E3mH2LZU7q9Z9lkJdA+6TnE=
-----END RSA PRIVATE KEY-----`,
};
test("multiple http request writes should not hang (issue #21620)", async () => {
const { promise, resolve, reject } = Promise.withResolvers<void>();
let responseReceived = false;
await using server = http.createServer((req, res) => {
let body = "";
req.on("data", chunk => {
body += chunk.toString();
});
req.on("end", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ receivedBody: body }));
});
});
server.listen(0);
await once(server, "listening");
const address = server.address() as AddressInfo;
const jsonStr = JSON.stringify({ key: "val", key2: 200 });
const req = http.request(
{
hostname: "localhost",
port: address.port,
method: "POST",
headers: {
"content-type": "application/json",
},
},
res => {
let data = "";
res.on("data", chunk => {
data += chunk.toString();
});
res.on("end", () => {
try {
const response = JSON.parse(data);
expect(response.receivedBody).toBe(jsonStr);
responseReceived = true;
resolve();
} catch (err) {
reject(err);
}
});
},
);
req.on("error", reject);
// Add timeout to prevent hanging - the issue was that this would timeout
const timeout = setTimeout(() => {
if (!responseReceived) {
req.destroy();
reject(new Error("Request timed out - indicates the hanging bug is present"));
}
}, 2000);
// Multiple writes should not cause hanging
req.write(jsonStr.slice(0, 10));
// Add small delay between writes to trigger the race condition
setTimeout(() => {
req.write(jsonStr.slice(10));
req.end();
}, 50);
try {
await promise;
clearTimeout(timeout);
} catch (err) {
clearTimeout(timeout);
throw err;
}
});
test("multiple https request writes should not hang (issue #21620)", async () => {
const { promise, resolve, reject } = Promise.withResolvers<void>();
let responseReceived = false;
await using server = https.createServer(COMMON_TLS_CERT, (req, res) => {
let body = "";
req.on("data", chunk => {
body += chunk.toString();
});
req.on("end", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ receivedBody: body }));
});
});
server.listen(0);
await once(server, "listening");
const address = server.address() as AddressInfo;
const jsonStr = JSON.stringify({ key: "val", key2: 200 });
const req = https.request(
{
hostname: "localhost",
port: address.port,
method: "POST",
headers: {
"content-type": "application/json",
},
rejectUnauthorized: false, // For test cert
},
res => {
let data = "";
res.on("data", chunk => {
data += chunk.toString();
});
res.on("end", () => {
try {
const response = JSON.parse(data);
expect(response.receivedBody).toBe(jsonStr);
responseReceived = true;
resolve();
} catch (err) {
reject(err);
}
});
},
);
req.on("error", reject);
// Add timeout to prevent hanging
const timeout = setTimeout(() => {
if (!responseReceived) {
req.destroy();
reject(new Error("HTTPS request timed out - indicates the hanging bug is present"));
}
}, 2000);
// Multiple writes should not cause hanging
req.write(jsonStr.slice(0, 10));
// Add small delay between writes to trigger the race condition
setTimeout(() => {
req.write(jsonStr.slice(10));
req.end();
}, 50);
try {
await promise;
clearTimeout(timeout);
} catch (err) {
clearTimeout(timeout);
throw err;
}
});