From 08baed00aeb92f30a13835d43b90ca9bbf2f52aa Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sat, 31 Jan 2026 19:25:28 +0000 Subject: [PATCH] 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 --- test/regression/issue/26638.test.ts | 206 +++++++++++++++------------- 1 file changed, 108 insertions(+), 98 deletions(-) diff --git a/test/regression/issue/26638.test.ts b/test/regression/issue/26638.test.ts index 391565471e..d8c01648de 100644 --- a/test/regression/issue/26638.test.ts +++ b/test/regression/issue/26638.test.ts @@ -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; });