Update tests to use HTTPS to properly exercise TLS handshake race condition

- Changed all servers to use TLS via Bun.serve({ tls })
- Changed clients to use node:https instead of node:http
- Added rejectUnauthorized: false for self-signed certs
- Added strictSSL: false for request-promise

This properly exercises the original issue which only occurs during
new HTTPS connections due to the TLS handshake timing gap.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-31 19:25:28 +00:00
parent 32747384b7
commit 08baed00ae

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { bunEnv, bunExe, tempDir, tls } from "harness";
// Test for GitHub issue #26638
// First multipart upload over HTTPS corrupts the body when using request-promise + fs.createReadStream()
@@ -8,12 +8,13 @@ import { bunEnv, bunExe, tempDir } from "harness";
describe("issue #26638", () => {
test("node:https streaming body yields all chunks even when end() is called quickly", async () => {
// This test simulates the race condition where:
// 1. Multiple chunks are written quickly to the ClientRequest
// 1. Multiple chunks are written quickly to the ClientRequest over HTTPS
// 2. The request is ended before all chunks have been yielded by the async generator
// The fix ensures that all buffered chunks are yielded after the finished flag is set.
using server = Bun.serve({
port: 0,
tls,
async fetch(req) {
const text = await req.text();
return new Response(
@@ -28,7 +29,7 @@ describe("issue #26638", () => {
const dashes = Buffer.alloc(100, "-").toString();
const clientScript = `
const http = require('http');
const https = require('https');
const chunks = [];
for (let i = 0; i < 100; i++) {
@@ -36,12 +37,13 @@ for (let i = 0; i < 100; i++) {
}
const expectedContent = chunks.join('');
const req = http.request('http://localhost:${server.port}/', {
const req = https.request('https://localhost:${server.port}/', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked',
},
rejectUnauthorized: false,
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
@@ -95,71 +97,76 @@ req.end();
expect(exitCode).toBe(0);
});
// This test requires a longer timeout because it installs npm packages (request, request-promise)
test("request-promise with form-data and fs.createReadStream works correctly", { timeout: 60_000 }, async () => {
// This test specifically reproduces the original issue:
// Using request-promise with form-data piping an fs.createReadStream
// This test installs npm packages which may take longer than the default timeout
test(
"request-promise with form-data and fs.createReadStream works correctly over HTTPS",
{ timeout: 60_000 },
async () => {
// This test specifically reproduces the original issue:
// Using request-promise with form-data piping an fs.createReadStream over HTTPS
using server = Bun.serve({
port: 0,
async fetch(req) {
try {
const formData = await req.formData();
const file = formData.get("sourceFile");
if (!(file instanceof Blob)) {
return new Response(JSON.stringify({ success: false, error: "No file found" }), {
status: 400,
using server = Bun.serve({
port: 0,
tls,
async fetch(req) {
try {
const formData = await req.formData();
const file = formData.get("sourceFile");
if (!(file instanceof Blob)) {
return new Response(JSON.stringify({ success: false, error: "No file found" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const content = await file.arrayBuffer();
return new Response(
JSON.stringify({
success: true,
bytesReceived: file.size,
// Verify content is correct (should be all 'A's)
contentValid: new Uint8Array(content).every(b => b === 65), // 65 is 'A'
}),
{ headers: { "Content-Type": "application/json" } },
);
} catch (e: unknown) {
let errorMessage: string;
if (e instanceof Error) {
errorMessage = e.message;
} else if (typeof e === "object" && e !== null && "message" in e) {
errorMessage = String((e as { message: unknown }).message);
} else {
errorMessage = String(e);
}
return new Response(JSON.stringify({ success: false, error: errorMessage }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
const content = await file.arrayBuffer();
return new Response(
JSON.stringify({
success: true,
bytesReceived: file.size,
// Verify content is correct (should be all 'A's)
contentValid: new Uint8Array(content).every(b => b === 65), // 65 is 'A'
}),
{ headers: { "Content-Type": "application/json" } },
);
} catch (e: unknown) {
let errorMessage: string;
if (e instanceof Error) {
errorMessage = e.message;
} else if (typeof e === "object" && e !== null && "message" in e) {
errorMessage = String((e as { message: unknown }).message);
} else {
errorMessage = String(e);
}
return new Response(JSON.stringify({ success: false, error: errorMessage }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
},
});
using dir = tempDir("test-26638-form", {
"package.json": JSON.stringify({
name: "test-26638",
dependencies: {
request: "^2.88.2",
"request-promise": "^4.2.6",
},
}),
// Create a test file with known content (100KB)
"testfile.txt": Buffer.alloc(1024 * 100, "A").toString(),
"client.js": `
});
using dir = tempDir("test-26638-form", {
"package.json": JSON.stringify({
name: "test-26638",
dependencies: {
request: "^2.88.2",
"request-promise": "^4.2.6",
},
}),
// Create a test file with known content (100KB)
"testfile.txt": Buffer.alloc(1024 * 100, "A").toString(),
"client.js": `
const fs = require('fs');
const request = require('request-promise');
async function upload() {
try {
const result = await request.post('http://localhost:${server.port}/', {
const result = await request.post('https://localhost:${server.port}/', {
formData: {
sourceFile: fs.createReadStream('./testfile.txt'),
},
json: true,
strictSSL: false,
});
console.log(JSON.stringify(result));
} catch (e) {
@@ -170,57 +177,59 @@ async function upload() {
upload();
`,
});
});
// Install dependencies
await using installProc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [installStdout, installStderr, installExitCode] = await Promise.all([
installProc.stdout.text(),
installProc.stderr.text(),
installProc.exited,
]);
if (installExitCode !== 0) {
console.error("Install stdout:", installStdout);
console.error("Install stderr:", installStderr);
}
expect(installExitCode).toBe(0);
// Install dependencies
await using installProc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [installStdout, installStderr, installExitCode] = await Promise.all([
installProc.stdout.text(),
installProc.stderr.text(),
installProc.exited,
]);
if (installExitCode !== 0) {
console.error("Install stdout:", installStdout);
console.error("Install stderr:", installStderr);
}
expect(installExitCode).toBe(0);
// Run the client
await using proc = Bun.spawn({
cmd: [bunExe(), "client.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
// Run the client
await using proc = Bun.spawn({
cmd: [bunExe(), "client.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (stderr) {
console.error("stderr:", stderr);
}
if (stderr) {
console.error("stderr:", stderr);
}
// Check stdout before exitCode for better error messages on test failure
expect(stdout.trim()).not.toBe("");
const result = JSON.parse(stdout.trim());
expect(result.success).toBe(true);
expect(result.bytesReceived).toBe(1024 * 100);
expect(result.contentValid).toBe(true);
expect(exitCode).toBe(0);
});
// Check stdout before exitCode for better error messages on test failure
expect(stdout.trim()).not.toBe("");
const result = JSON.parse(stdout.trim());
expect(result.success).toBe(true);
expect(result.bytesReceived).toBe(1024 * 100);
expect(result.contentValid).toBe(true);
expect(exitCode).toBe(0);
},
);
test("multiple rapid writes followed by immediate end() yields all data", async () => {
test("multiple rapid writes followed by immediate end() yields all data over HTTPS", async () => {
// This test ensures that when many writes happen in quick succession
// followed by an immediate end(), no data is lost.
// followed by an immediate end() over HTTPS, no data is lost.
using server = Bun.serve({
port: 0,
tls,
async fetch(req) {
const text = await req.text();
return new Response(
@@ -235,18 +244,19 @@ upload();
const chunkContent = Buffer.alloc(100, "X").toString();
const clientScript = `
const http = require('http');
const https = require('https');
const numChunks = 1000;
const chunkSize = 100;
const expectedLength = numChunks * chunkSize;
const req = http.request('http://localhost:${server.port}/', {
const req = https.request('https://localhost:${server.port}/', {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'Transfer-Encoding': 'chunked',
},
rejectUnauthorized: false,
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });