import { describe, expect, it } from "bun:test"; import { bunEnv, bunExe, tmpdirSync } from "harness"; import { join } from "node:path"; import tls from "node:tls"; type TLSOptions = { cert: string; key: string; passphrase?: string; }; import { expiredTls, invalidTls, tls as validTls } from "harness"; const CERT_LOCALHOST_IP = { ...validTls }; const CERT_EXPIRED = { ...expiredTls }; // Note: Do not use bun.sh as the example domain // Cloudflare sometimes blocks automated requests to it. // so it will cause flaky tests. async function createServer(cert: TLSOptions, callback: (port: number) => Promise) { using server = Bun.serve({ port: 0, tls: cert, fetch() { return new Response("Hello World"); }, }); await callback(server.port); } describe.concurrent("fetch-tls", () => { it("can handle multiple requests with non native checkServerIdentity", async () => { async function request() { let called = false; const result = await fetch("https://www.example.com", { keepalive: false, tls: { checkServerIdentity(hostname: string, cert: tls.PeerCertificate) { called = true; return tls.checkServerIdentity(hostname, cert); }, }, }).then((res: Response) => res.blob()); expect(result?.size).toBeGreaterThan(0); expect(called).toBe(true); } const promises = []; for (let i = 0; i < 5; i++) { promises.push(request()); } await Promise.all(promises); }); it("fetch with valid tls should not throw", async () => { const promises = [`https://example.com`, `https://www.example.com`].map(async url => { const result = await fetch(url, { keepalive: false }).then((res: Response) => res.blob()); expect(result?.size).toBeGreaterThan(0); }); await Promise.all(promises); }); it("fetch with valid tls and non-native checkServerIdentity should work", async () => { for (const isBusy of [true, false]) { let count = 0; const promises = [`https://example.com`, `https://www.example.com`].map(async url => { await fetch(url, { keepalive: false, tls: { checkServerIdentity(hostname: string, cert: tls.PeerCertificate) { count++; expect(url).toContain(hostname); return tls.checkServerIdentity(hostname, cert); }, }, }).then((res: Response) => res.blob()); }); if (isBusy) { const start = performance.now(); while (performance.now() - start < 500) {} } await Promise.all(promises); expect(count).toBe(2); } }); it("fetch with valid tls and non-native checkServerIdentity should work", async () => { let count = 0; const promises = [`https://example.com`, `https://www.example.com`].map(async url => { await fetch(url, { keepalive: false, tls: { checkServerIdentity(hostname: string, cert: tls.PeerCertificate) { count++; expect(url).toContain(hostname); throw new Error("CustomError"); }, }, }); }); const start = performance.now(); while (performance.now() - start < 1000) {} expect((await Promise.allSettled(promises)).every(p => p.status === "rejected")).toBe(true); expect(count).toBe(2); }); it("fetch with rejectUnauthorized: false should not call checkServerIdentity", async () => { let count = 0; await fetch("https://example.com", { keepalive: false, tls: { rejectUnauthorized: false, checkServerIdentity(hostname: string, cert: tls.PeerCertificate) { count++; return tls.checkServerIdentity(hostname, cert); }, }, }).then((res: Response) => res.blob()); expect(count).toBe(0); }); it("fetch with self-sign tls should throw", async () => { await createServer(CERT_LOCALHOST_IP, async port => { const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`]; await Promise.all( urls.map(async url => { try { await fetch(url).then((res: Response) => res.blob()); expect.unreachable(); } catch (e: any) { expect(e.code).toBe("DEPTH_ZERO_SELF_SIGNED_CERT"); } }), ); }); }); it("fetch with invalid tls should throw", async () => { await createServer(CERT_EXPIRED, async port => { await Promise.all( [`https://localhost:${port}`, `https://127.0.0.1:${port}`].map(async url => { try { await fetch(url).then((res: Response) => res.blob()); expect.unreachable(); } catch (e: any) { expect(e.code).toBe("CERT_HAS_EXPIRED"); } }), ); }); }); it("fetch with checkServerIdentity failing should throw", async () => { try { await fetch(`https://example.com`, { keepalive: false, tls: { checkServerIdentity() { return new Error("CustomError"); }, }, }).then((res: Response) => res.blob()); expect.unreachable(); } catch (e: any) { expect(e.message).toBe("CustomError"); } }); it("fetch with self-sign certificate tls + rejectUnauthorized: false should not throw", async () => { await createServer(CERT_LOCALHOST_IP, async port => { const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`]; await Promise.all( urls.map(async url => { try { const result = await fetch(url, { tls: { rejectUnauthorized: false } }).then((res: Response) => res.text()); expect(result).toBe("Hello World"); } catch { expect.unreachable(); } }), ); }); }); it("fetch with invalid tls + rejectUnauthorized: false should not throw", async () => { await createServer(CERT_EXPIRED, async port => { const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`]; await Promise.all( urls.map(async url => { try { const result = await fetch(url, { tls: { rejectUnauthorized: false } }).then((res: Response) => res.text()); expect(result).toBe("Hello World"); } catch (e) { expect.unreachable(); } }), ); }); }); it("fetch should respect rejectUnauthorized env", async () => { await createServer(CERT_EXPIRED, async port => { const url = `https://localhost:${port}`; const promises = []; for (let i = 0; i < 2; i++) { const proc = Bun.spawn({ env: { ...bunEnv, SERVER: url, NODE_TLS_REJECT_UNAUTHORIZED: i.toString(), }, stderr: "inherit", stdout: "inherit", stdin: "inherit", cmd: [bunExe(), join(import.meta.dir, "fetch-reject-authorized-env-fixture.js")], }); promises.push(proc.exited); } const [exitCode1, exitCode2] = await Promise.all(promises); expect(exitCode1).toBe(0); expect(exitCode2).toBe(1); }); }); it("fetch timeout works on tls", async () => { using server = Bun.serve({ tls: validTls, hostname: "localhost", port: 0, rejectUnauthorized: false, async fetch() { async function* body() { yield "Hello, "; await Bun.sleep(700); // should only take 200ms-350ms yield "World!"; } return new Response(body); }, }); const start = performance.now(); const TIMEOUT = 200; const THRESHOLD = 150; try { await fetch(server.url, { signal: AbortSignal.timeout(TIMEOUT), tls: { ca: validTls.cert }, }).then(res => res.text()); expect.unreachable(); } catch (e) { expect(e.name).toBe("TimeoutError"); } finally { const total = performance.now() - start; expect(total).toBeGreaterThanOrEqual(TIMEOUT - THRESHOLD); expect(total).toBeLessThanOrEqual(TIMEOUT + THRESHOLD); } }); it("fetch should use NODE_EXTRA_CA_CERTS", async () => { using server = Bun.serve({ port: 0, tls: validTls, fetch() { return new Response("OK"); }, }); const cert_path = join(tmpdirSync(), "cert.pem"); await Bun.write(cert_path, validTls.cert); const proc = Bun.spawn({ env: { ...bunEnv, SERVER: server.url, NODE_EXTRA_CA_CERTS: cert_path, }, stderr: "inherit", stdout: "inherit", stdin: "inherit", cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")], }); expect(await proc.exited).toBe(0); }); it("fetch should use NODE_EXTRA_CA_CERTS even if the used CA is not first in bundle", async () => { using server = Bun.serve({ port: 0, tls: validTls, fetch() { return new Response("OK"); }, }); const bundlePath = join(tmpdirSync(), "bundle.pem"); const bundleContent = `${expiredTls.cert}\n${validTls.cert}`; await Bun.write(bundlePath, bundleContent); const proc = Bun.spawn({ env: { ...bunEnv, SERVER: server.url, NODE_EXTRA_CA_CERTS: bundlePath, }, stderr: "inherit", stdout: "inherit", stdin: "inherit", cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")], }); expect(await proc.exited).toBe(0); }); it("fetch should ignore invalid NODE_EXTRA_CA_CERTS", async () => { using server = Bun.serve({ port: 0, tls: validTls, fetch() { return new Response("OK"); }, }); for (const invalid of ["not-exist.pem", "", " "]) { const proc = Bun.spawn({ env: { ...bunEnv, SERVER: server.url, NODE_EXTRA_CA_CERTS: invalid, }, stderr: "pipe", stdout: "inherit", stdin: "inherit", cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")], }); expect(await proc.exited).toBe(1); expect(await proc.stderr.text()).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); } }); it("fetch should ignore NODE_EXTRA_CA_CERTS if it's contains invalid cert", async () => { using server = Bun.serve({ port: 0, tls: validTls, fetch() { return new Response("OK"); }, }); const mixedValidAndInvalidCertsBundlePath = join(tmpdirSync(), "mixed-valid-and-invalid-certs-bundle.pem"); await Bun.write(mixedValidAndInvalidCertsBundlePath, `${invalidTls.cert}\n${validTls.cert}`); const mixedInvalidAndValidCertsBundlePath = join(tmpdirSync(), "mixed-invalid-and-valid-certs-bundle.pem"); await Bun.write(mixedInvalidAndValidCertsBundlePath, `${validTls.cert}\n${invalidTls.cert}`); for (const invalid of [mixedValidAndInvalidCertsBundlePath, mixedInvalidAndValidCertsBundlePath]) { const proc = Bun.spawn({ env: { ...bunEnv, SERVER: server.url, NODE_EXTRA_CA_CERTS: invalid, }, stderr: "pipe", stdout: "inherit", stdin: "inherit", cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")], }); expect(await proc.exited).toBe(1); const stderr = await proc.stderr.text(); expect(stderr).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); expect(stderr).toContain("ignoring extra certs"); } }); });