mirror of
https://github.com/oven-sh/bun
synced 2026-02-07 09:28:51 +00:00
Compare commits
9 Commits
dylan/pyth
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc5c7918e | ||
|
|
dd2421c72b | ||
|
|
ffb3b3ea89 | ||
|
|
7d71e6034c | ||
|
|
610ff1acd7 | ||
|
|
e66cf208c8 | ||
|
|
ed98720cac | ||
|
|
217a02d422 | ||
|
|
d0ca971a2b |
17
bun.lock
17
bun.lock
@@ -3,6 +3,9 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bun",
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.3.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/common": "^1.2.3",
|
||||
"@lezer/cpp": "^1.1.3",
|
||||
@@ -40,8 +43,8 @@
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"bun-types": "workspace:packages/bun-types",
|
||||
"@types/bun": "workspace:packages/@types/bun",
|
||||
"bun-types": "workspace:packages/bun-types",
|
||||
},
|
||||
"packages": {
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||
@@ -194,6 +197,8 @@
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
@@ -204,6 +209,10 @@
|
||||
|
||||
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
||||
|
||||
"header-case": ["header-case@2.0.4", "", { "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" } }, "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q=="],
|
||||
@@ -270,6 +279,10 @@
|
||||
|
||||
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"octokit": ["octokit@3.2.2", "", { "dependencies": { "@octokit/app": "^14.0.2", "@octokit/core": "^5.0.0", "@octokit/oauth-app": "^6.0.0", "@octokit/plugin-paginate-graphql": "^4.0.0", "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1", "@octokit/plugin-retry": "^6.0.0", "@octokit/plugin-throttling": "^8.0.0", "@octokit/request-error": "^5.0.0", "@octokit/types": "^13.0.0", "@octokit/webhooks": "^12.3.1" } }, "sha512-7Abo3nADdja8l/aglU6Y3lpnHSfv0tw7gFPiqzry/yCU+2gTAX7R1roJ8hJrxIK+S1j+7iqRJXtmuHJ/UDsBhQ=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
@@ -322,6 +335,8 @@
|
||||
|
||||
"upper-case-first": ["upper-case-first@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"@octokit/app/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="],
|
||||
|
||||
@@ -85,5 +85,8 @@
|
||||
"node:test:cp": "bun ./scripts/fetch-node-test.ts ",
|
||||
"clean:zig": "rm -rf build/debug/cache/zig build/debug/CMakeCache.txt 'build/debug/*.o' .zig-cache zig-out || true",
|
||||
"sync-webkit-source": "bun ./scripts/sync-webkit-source.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +490,12 @@ function ClientRequest(input, options, cb) {
|
||||
}
|
||||
|
||||
try {
|
||||
options.lookup(host, { all: true }, (err, results) => {
|
||||
const lookupOptions = { all: true };
|
||||
// Pass family option from agent if available
|
||||
if (this[kAgent]?.options?.family) {
|
||||
lookupOptions.family = this[kAgent].options.family;
|
||||
}
|
||||
options.lookup(host, lookupOptions, (err, results) => {
|
||||
if (err) {
|
||||
if (!!$debug) globalReportError(err);
|
||||
process.nextTick((self, err) => self.emit("error", err), this, err);
|
||||
@@ -635,6 +640,12 @@ function ClientRequest(input, options, cb) {
|
||||
options = ObjectAssign(input || {}, options);
|
||||
}
|
||||
|
||||
// Set default DNS lookup if not provided
|
||||
if (!options.lookup) {
|
||||
const dns = require("node:dns");
|
||||
options.lookup = dns.lookup;
|
||||
}
|
||||
|
||||
this[kTls] = null;
|
||||
this[kAbortController] = null;
|
||||
|
||||
|
||||
109
src/js/thirdparty/node-fetch.ts
vendored
109
src/js/thirdparty/node-fetch.ts
vendored
@@ -146,8 +146,16 @@ class Request extends WebRequest {
|
||||
* It's overall a positive on speed to override the implementation, since most people will use something
|
||||
* like `.json()` or `.text()`, which is faster in Bun's native fetch, vs `node-fetch` going
|
||||
* through `node:http`, a node stream, then processing the data.
|
||||
*
|
||||
* However, when an agent with family option is provided, we fall back to node:https
|
||||
* to ensure proper IPv6/IPv4 DNS resolution.
|
||||
*/
|
||||
async function fetch(url: any, init?: RequestInit & { body?: any }) {
|
||||
async function fetch(url: any, init?: RequestInit & { body?: any; agent?: any }) {
|
||||
// Check if agent with family option is provided - if so, use node:https for proper DNS handling
|
||||
if (init?.agent && init.agent.options && typeof init.agent.options.family === "number") {
|
||||
return await fetchViaNodeHttps(url, init);
|
||||
}
|
||||
|
||||
// input node stream -> web stream
|
||||
let body: s.Readable | undefined = init?.body;
|
||||
if (body) {
|
||||
@@ -167,6 +175,105 @@ async function fetch(url: any, init?: RequestInit & { body?: any }) {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback implementation using node:https when agent family option is needed.
|
||||
* This ensures proper IPv6/IPv4 DNS resolution that nativeFetch doesn't support.
|
||||
*/
|
||||
async function fetchViaNodeHttps(url: any, init: any): Promise<Response> {
|
||||
const https = require("node:https");
|
||||
const http = require("node:http");
|
||||
const { Readable } = require("node:stream");
|
||||
|
||||
// Parse URL
|
||||
const urlObj = new URL(url);
|
||||
const isHttps = urlObj.protocol === "https:";
|
||||
const request = isHttps ? https.request : http.request;
|
||||
|
||||
// Convert init to node:https options
|
||||
const options: any = {
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || (isHttps ? 443 : 80),
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: init?.method || "GET",
|
||||
headers: {},
|
||||
agent: init?.agent,
|
||||
};
|
||||
|
||||
// Convert headers
|
||||
if (init?.headers) {
|
||||
if (init.headers instanceof Headers) {
|
||||
for (const [key, value] of init.headers.entries()) {
|
||||
options.headers[key] = value;
|
||||
}
|
||||
} else if (typeof init.headers === "object") {
|
||||
Object.assign(options.headers, init.headers);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = request(options, (res: any) => {
|
||||
const chunks: any[] = [];
|
||||
|
||||
res.on("data", (chunk: any) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
res.on("end", () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
|
||||
// Convert Node.js response to fetch Response
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(res.headers)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => headers.append(key, v));
|
||||
} else if (typeof value === "string") {
|
||||
headers.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const response = new Response(body, {
|
||||
status: res.statusCode,
|
||||
statusText: res.statusMessage,
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
Object.setPrototypeOf(response, ResponsePrototype);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
|
||||
// Handle request body
|
||||
if (init?.body) {
|
||||
if (typeof init.body === "string") {
|
||||
req.write(init.body);
|
||||
req.end();
|
||||
} else if (init.body instanceof Buffer) {
|
||||
req.write(init.body);
|
||||
req.end();
|
||||
} else if (init.body instanceof Readable) {
|
||||
init.body.pipe(req);
|
||||
return; // Don't call req.end() - pipe will do it
|
||||
} else if (init.body instanceof Blob) {
|
||||
// Handle Blob body asynchronously
|
||||
init.body
|
||||
.arrayBuffer()
|
||||
.then(arrayBuffer => {
|
||||
req.write(Buffer.from(arrayBuffer));
|
||||
req.end();
|
||||
})
|
||||
.catch(reject);
|
||||
return;
|
||||
} else {
|
||||
req.end();
|
||||
}
|
||||
} else {
|
||||
req.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class AbortError extends DOMException {
|
||||
constructor(message) {
|
||||
super(message, "AbortError");
|
||||
|
||||
1
test/js/node/http/.gitignore
vendored
Normal file
1
test/js/node/http/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
24
test/js/node/http/bun.lock
Normal file
24
test/js/node/http/bun.lock
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "node-http-tests",
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.3.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
}
|
||||
}
|
||||
127
test/js/node/http/https-agent-family-node-compat.test.ts
Normal file
127
test/js/node/http/https-agent-family-node-compat.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import https from "node:https";
|
||||
|
||||
describe("https.Agent family option Node.js compatibility", () => {
|
||||
test("DNS lookup should receive options in Node.js compatible format", () => {
|
||||
// Verify that our implementation matches Node.js behavior for DNS lookup options
|
||||
let capturedOptions: any = null;
|
||||
|
||||
const mockLookup = (hostname: string, options: any, callback: any) => {
|
||||
capturedOptions = { ...options };
|
||||
|
||||
// Mock response matching Node.js format
|
||||
if (options.family === 6) {
|
||||
callback(null, [
|
||||
{ address: "2001:db8::1", family: 6 },
|
||||
{ address: "2001:db8::2", family: 6 },
|
||||
]);
|
||||
} else if (options.family === 4) {
|
||||
callback(null, [
|
||||
{ address: "192.0.2.1", family: 4 },
|
||||
{ address: "192.0.2.2", family: 4 },
|
||||
]);
|
||||
} else {
|
||||
callback(null, [
|
||||
{ address: "192.0.2.1", family: 4 },
|
||||
{ address: "2001:db8::1", family: 6 },
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "example.test",
|
||||
path: "/",
|
||||
agent: agent,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
req.on("error", () => {}); // Ignore connection errors
|
||||
req.end();
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(capturedOptions).not.toBeNull();
|
||||
expect(capturedOptions.family).toBe(6);
|
||||
expect(capturedOptions.all).toBe(true);
|
||||
|
||||
// Verify the format matches Node.js expectations
|
||||
expect(typeof capturedOptions.family).toBe("number");
|
||||
expect(typeof capturedOptions.all).toBe("boolean");
|
||||
|
||||
req.destroy();
|
||||
resolve(undefined);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
test("Agent.getName should be compatible with Node.js format", () => {
|
||||
const agent6 = new https.Agent({ family: 6 });
|
||||
const agent4 = new https.Agent({ family: 4 });
|
||||
|
||||
// Test that getName includes family like Node.js does
|
||||
const name6 = agent6.getName({ host: "example.com", port: 443, family: 6 });
|
||||
const name4 = agent4.getName({ host: "example.com", port: 443, family: 4 });
|
||||
|
||||
// Should include family in name for connection pooling compatibility
|
||||
expect(name6).toMatch(/.*:6$/);
|
||||
expect(name4).toMatch(/.*:4$/);
|
||||
|
||||
// Format should be consistent
|
||||
expect(name6).toContain("example.com:443");
|
||||
expect(name4).toContain("example.com:443");
|
||||
});
|
||||
|
||||
test("Agent options should store family value like Node.js", () => {
|
||||
// Test different family values that Node.js supports
|
||||
const agent6 = new https.Agent({ family: 6 });
|
||||
const agent4 = new https.Agent({ family: 4 });
|
||||
const agent0 = new https.Agent({ family: 0 }); // Node.js supports 0 for dual stack
|
||||
const agentDefault = new https.Agent();
|
||||
|
||||
expect(agent6.options.family).toBe(6);
|
||||
expect(agent4.options.family).toBe(4);
|
||||
expect(agent0.options.family).toBe(0);
|
||||
expect(agentDefault.options.family).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should maintain Node.js behavior for IP address hostnames", () => {
|
||||
// When hostname is already an IP, DNS lookup should be skipped
|
||||
let lookupCalled = false;
|
||||
|
||||
const mockLookup = () => {
|
||||
lookupCalled = true;
|
||||
throw new Error("Lookup should not be called for IP addresses");
|
||||
};
|
||||
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
|
||||
// IPv4 address should skip lookup despite IPv6 family setting
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
path: "/",
|
||||
port: 8443,
|
||||
agent: agent,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
req.on("error", () => {}); // Ignore connection error
|
||||
req.end();
|
||||
|
||||
// Give it time to potentially call lookup
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(lookupCalled).toBe(false);
|
||||
req.destroy();
|
||||
resolve(undefined);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
});
|
||||
115
test/js/node/http/https-agent-family-scope.test.ts
Normal file
115
test/js/node/http/https-agent-family-scope.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import https from "node:https";
|
||||
|
||||
describe("https.Agent family option scope and limitations", () => {
|
||||
test("https.request should respect family option (FIXED)", () => {
|
||||
// This test verifies that our fix works for https.request
|
||||
let lookupOptionsReceived: any = null;
|
||||
|
||||
const mockLookup = (hostname: string, options: any, callback: any) => {
|
||||
lookupOptionsReceived = { ...options };
|
||||
// Return IPv6 mock result
|
||||
callback(null, [{ address: "2001:db8::1", family: 6 }]);
|
||||
};
|
||||
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "test.example",
|
||||
path: "/",
|
||||
agent: agent,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
req.on("error", () => {}); // Ignore connection errors
|
||||
req.end();
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(lookupOptionsReceived).not.toBeNull();
|
||||
expect(lookupOptionsReceived.family).toBe(6);
|
||||
expect(lookupOptionsReceived.all).toBe(true);
|
||||
req.destroy();
|
||||
resolve(undefined);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
test("https.request with string URL should respect family option (FIXED)", () => {
|
||||
// This test verifies string URL + options pattern works (used by node-fetch)
|
||||
let lookupOptionsReceived: any = null;
|
||||
|
||||
const mockLookup = (hostname: string, options: any, callback: any) => {
|
||||
lookupOptionsReceived = { ...options };
|
||||
callback(null, [{ address: "2001:db8::1", family: 6 }]);
|
||||
};
|
||||
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
|
||||
const req = https.request(
|
||||
"https://test.example/",
|
||||
{
|
||||
agent: agent,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
req.on("error", () => {});
|
||||
req.end();
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(lookupOptionsReceived).not.toBeNull();
|
||||
expect(lookupOptionsReceived.family).toBe(6);
|
||||
expect(lookupOptionsReceived.all).toBe(true);
|
||||
req.destroy();
|
||||
resolve(undefined);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
test("built-in fetch() does not support agent parameter (LIMITATION)", () => {
|
||||
// This documents the current limitation - built-in fetch ignores agent
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
|
||||
// This should not throw, but the agent will be ignored
|
||||
expect(() => {
|
||||
fetch("https://example.com", { agent } as any);
|
||||
}).not.toThrow();
|
||||
|
||||
// Note: The agent parameter is silently ignored by Bun's native fetch
|
||||
// This would need to be fixed separately in Bun's fetch implementation
|
||||
});
|
||||
|
||||
test("node-fetch package uses Bun.fetch internally (LIMITATION)", () => {
|
||||
// This documents that node-fetch in Bun doesn't use our fixed https.request
|
||||
// Instead it uses Bun's native fetch which doesn't support agent.family
|
||||
|
||||
// We can't easily test this without actually making network requests
|
||||
// but this test documents the expected behavior
|
||||
expect(true).toBe(true); // Placeholder test
|
||||
|
||||
// Note: node-fetch in Bun is overridden to use Bun.fetch for performance
|
||||
// This means it bypasses the node:https module entirely
|
||||
// Agent support for node-fetch would need to be implemented in Bun.fetch
|
||||
});
|
||||
|
||||
test("Agent.getName includes family for connection pooling", () => {
|
||||
// Verify this works correctly for connection pooling
|
||||
const agent6 = new https.Agent({ family: 6 });
|
||||
const agent4 = new https.Agent({ family: 4 });
|
||||
|
||||
const name6 = agent6.getName({ host: "example.com", port: 443, family: 6 });
|
||||
const name4 = agent4.getName({ host: "example.com", port: 443, family: 4 });
|
||||
|
||||
expect(name6).toContain(":6");
|
||||
expect(name4).toContain(":4");
|
||||
|
||||
// Different family should result in different connection names
|
||||
expect(name6).not.toBe(name4);
|
||||
});
|
||||
});
|
||||
72
test/js/node/http/https-agent-family-simple.test.ts
Normal file
72
test/js/node/http/https-agent-family-simple.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import https from "node:https";
|
||||
|
||||
describe("https.Agent family option (no network)", () => {
|
||||
test("Agent should store family option correctly", () => {
|
||||
const agent6 = new https.Agent({ family: 6 });
|
||||
expect(agent6.options.family).toBe(6);
|
||||
|
||||
const agent4 = new https.Agent({ family: 4 });
|
||||
expect(agent4.options.family).toBe(4);
|
||||
|
||||
const agentDefault = new https.Agent();
|
||||
expect(agentDefault.options.family).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Agent.getName should include family in connection name", () => {
|
||||
const agent6 = new https.Agent({ family: 6 });
|
||||
const agent4 = new https.Agent({ family: 4 });
|
||||
|
||||
const name6 = agent6.getName({ host: "example.com", port: 443, family: 6 });
|
||||
const name4 = agent4.getName({ host: "example.com", port: 443, family: 4 });
|
||||
const nameDefault = agent6.getName({ host: "example.com", port: 443 });
|
||||
|
||||
expect(name6).toContain(":6");
|
||||
expect(name4).toContain(":4");
|
||||
// Without family parameter, should not include family in name
|
||||
expect(nameDefault).not.toMatch(/:6$/);
|
||||
});
|
||||
|
||||
test("DNS lookup function gets set by default in ClientRequest", () => {
|
||||
// This test verifies that our fix to set options.lookup works
|
||||
let lookupWasCalled = false;
|
||||
let capturedLookupOptions: any = null;
|
||||
|
||||
const mockLookup = (hostname: string, options: any, callback: any) => {
|
||||
lookupWasCalled = true;
|
||||
capturedLookupOptions = { ...options };
|
||||
// Call callback with error to avoid actual connection
|
||||
callback(new Error("Test DNS error"));
|
||||
};
|
||||
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "test.example",
|
||||
path: "/",
|
||||
agent: agent,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
res => {},
|
||||
);
|
||||
|
||||
req.on("error", err => {
|
||||
// Ignore the test DNS error
|
||||
});
|
||||
|
||||
req.end();
|
||||
|
||||
// Give it a moment for the lookup to be called
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(lookupWasCalled).toBe(true);
|
||||
expect(capturedLookupOptions).not.toBeNull();
|
||||
expect(capturedLookupOptions.family).toBe(6);
|
||||
expect(capturedLookupOptions.all).toBe(true);
|
||||
req.destroy();
|
||||
resolve(undefined);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
});
|
||||
141
test/js/node/http/https-agent-family.test.ts
Normal file
141
test/js/node/http/https-agent-family.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import dns from "node:dns";
|
||||
import https from "node:https";
|
||||
|
||||
describe("https.Agent family option", () => {
|
||||
test("should pass family option to DNS lookup", async () => {
|
||||
// Mock DNS lookup to verify family option is passed through
|
||||
let capturedOptions: any = null;
|
||||
const originalLookup = dns.lookup;
|
||||
|
||||
// Mock lookup function to capture options
|
||||
const mockLookup = (hostname: string, options: any, callback: any) => {
|
||||
capturedOptions = { ...options };
|
||||
// Call the real lookup to get actual results
|
||||
originalLookup(hostname, options, callback);
|
||||
};
|
||||
|
||||
try {
|
||||
// Test with family: 6
|
||||
const agent6 = new https.Agent({ family: 6 });
|
||||
expect(agent6.options.family).toBe(6);
|
||||
|
||||
// Create request that should use DNS lookup
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "example.com", // Use a hostname that requires DNS lookup
|
||||
path: "/",
|
||||
agent: agent6,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
res => {
|
||||
// Don't need to handle response for this test
|
||||
},
|
||||
);
|
||||
|
||||
// End the request to trigger DNS lookup
|
||||
req.end();
|
||||
|
||||
// Wait for DNS lookup to be called
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Verify family option was passed to DNS lookup
|
||||
expect(capturedOptions).not.toBeNull();
|
||||
expect(capturedOptions.family).toBe(6);
|
||||
expect(capturedOptions.all).toBe(true);
|
||||
|
||||
req.destroy();
|
||||
} finally {
|
||||
// Restore original lookup
|
||||
dns.lookup = originalLookup;
|
||||
}
|
||||
});
|
||||
|
||||
test("should pass family: 4 option to DNS lookup", async () => {
|
||||
let capturedOptions: any = null;
|
||||
const originalLookup = dns.lookup;
|
||||
|
||||
const mockLookup = (hostname: string, options: any, callback: any) => {
|
||||
capturedOptions = { ...options };
|
||||
originalLookup(hostname, options, callback);
|
||||
};
|
||||
|
||||
try {
|
||||
const agent4 = new https.Agent({ family: 4 });
|
||||
expect(agent4.options.family).toBe(4);
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "example.com",
|
||||
path: "/",
|
||||
agent: agent4,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
res => {},
|
||||
);
|
||||
|
||||
req.end();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(capturedOptions).not.toBeNull();
|
||||
expect(capturedOptions.family).toBe(4);
|
||||
expect(capturedOptions.all).toBe(true);
|
||||
|
||||
req.destroy();
|
||||
} finally {
|
||||
dns.lookup = originalLookup;
|
||||
}
|
||||
});
|
||||
|
||||
test("should not pass family option when not specified", async () => {
|
||||
let capturedOptions: any = null;
|
||||
const originalLookup = dns.lookup;
|
||||
|
||||
const mockLookup = (hostname: string, options: any, callback: any) => {
|
||||
capturedOptions = { ...options };
|
||||
originalLookup(hostname, options, callback);
|
||||
};
|
||||
|
||||
try {
|
||||
const agent = new https.Agent(); // No family specified
|
||||
expect(agent.options.family).toBeUndefined();
|
||||
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "example.com",
|
||||
path: "/",
|
||||
agent: agent,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
res => {},
|
||||
);
|
||||
|
||||
req.end();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(capturedOptions).not.toBeNull();
|
||||
expect(capturedOptions.family).toBeUndefined();
|
||||
expect(capturedOptions.all).toBe(true);
|
||||
|
||||
req.destroy();
|
||||
} finally {
|
||||
dns.lookup = originalLookup;
|
||||
}
|
||||
});
|
||||
|
||||
test("should work with different hosts and preserve agent family setting", () => {
|
||||
const agent6 = new https.Agent({ family: 6 });
|
||||
const agent4 = new https.Agent({ family: 4 });
|
||||
|
||||
// Test that agent maintains its family setting
|
||||
expect(agent6.options.family).toBe(6);
|
||||
expect(agent4.options.family).toBe(4);
|
||||
|
||||
// Test that agent name includes family for connection pooling
|
||||
const name6 = agent6.getName({ host: "example.com", port: 443, family: 6 });
|
||||
const name4 = agent4.getName({ host: "example.com", port: 443, family: 4 });
|
||||
|
||||
expect(name6).toContain(":6");
|
||||
expect(name4).toContain(":4");
|
||||
});
|
||||
});
|
||||
177
test/js/node/http/node-fetch-agent-family.test.ts
Normal file
177
test/js/node/http/node-fetch-agent-family.test.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import fetch from "node-fetch";
|
||||
import https from "node:https";
|
||||
|
||||
describe("node-fetch agent family support", () => {
|
||||
test("should use https.request when agent with family is provided", () => {
|
||||
// Mock https.request to verify it gets called
|
||||
let httpsRequestCalled = false;
|
||||
let capturedOptions: any = null;
|
||||
|
||||
const originalRequest = https.request;
|
||||
https.request = function (...args: any[]) {
|
||||
httpsRequestCalled = true;
|
||||
capturedOptions = args[0]; // First argument is the options object
|
||||
|
||||
// Return a mock request that immediately errors to avoid network calls
|
||||
const mockReq = {
|
||||
on: () => mockReq,
|
||||
write: () => {},
|
||||
end: () => {
|
||||
// Simulate immediate error to avoid hanging
|
||||
process.nextTick(() => {
|
||||
if (mockReq.errorCallback) {
|
||||
mockReq.errorCallback(new Error("Mock connection error"));
|
||||
}
|
||||
});
|
||||
},
|
||||
errorCallback: null as any,
|
||||
};
|
||||
|
||||
// Override on('error') to capture the callback
|
||||
const originalOn = mockReq.on;
|
||||
mockReq.on = function (event: string, callback: any) {
|
||||
if (event === "error") {
|
||||
mockReq.errorCallback = callback;
|
||||
}
|
||||
return originalOn.call(this);
|
||||
};
|
||||
|
||||
return mockReq as any;
|
||||
};
|
||||
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
|
||||
return fetch("https://test.example/", { agent })
|
||||
.catch(err => {
|
||||
// Ignore the mock error, we just want to verify behavior
|
||||
})
|
||||
.finally(() => {
|
||||
// Restore original function
|
||||
https.request = originalRequest;
|
||||
|
||||
// Verify https.request was called
|
||||
expect(httpsRequestCalled).toBe(true);
|
||||
expect(capturedOptions).not.toBeNull();
|
||||
expect(capturedOptions.agent).toBe(agent);
|
||||
});
|
||||
});
|
||||
|
||||
test("should use native fetch when no agent is provided", async () => {
|
||||
// Mock https.request to verify it doesn't get called
|
||||
let httpsRequestCalled = false;
|
||||
|
||||
const originalRequest = https.request;
|
||||
https.request = function (...args: any[]) {
|
||||
httpsRequestCalled = true;
|
||||
return originalRequest.apply(this, args);
|
||||
};
|
||||
|
||||
try {
|
||||
// This should use native Bun.fetch, not https.request
|
||||
await fetch("https://httpbin.org/status/200");
|
||||
} catch (err) {
|
||||
// Ignore any connection errors
|
||||
} finally {
|
||||
// Restore original function
|
||||
https.request = originalRequest;
|
||||
}
|
||||
|
||||
// Verify https.request was NOT called
|
||||
expect(httpsRequestCalled).toBe(false);
|
||||
});
|
||||
|
||||
test("should use native fetch when agent exists but has no family option", async () => {
|
||||
// Mock https.request to verify it doesn't get called
|
||||
let httpsRequestCalled = false;
|
||||
|
||||
const originalRequest = https.request;
|
||||
https.request = function (...args: any[]) {
|
||||
httpsRequestCalled = true;
|
||||
return originalRequest.apply(this, args);
|
||||
};
|
||||
|
||||
// Agent without family option should use native fetch
|
||||
const agent = new https.Agent({ keepAlive: true });
|
||||
|
||||
try {
|
||||
await fetch("https://httpbin.org/status/200", { agent });
|
||||
} catch (err) {
|
||||
// Ignore any connection errors
|
||||
} finally {
|
||||
// Restore original function
|
||||
https.request = originalRequest;
|
||||
}
|
||||
|
||||
// Verify https.request was NOT called
|
||||
expect(httpsRequestCalled).toBe(false);
|
||||
});
|
||||
|
||||
test("should handle IPv4 family value", () => {
|
||||
let capturedOptions: any = null;
|
||||
|
||||
const originalRequest = https.request;
|
||||
https.request = function (...args: any[]) {
|
||||
capturedOptions = args[0];
|
||||
|
||||
const mockReq = {
|
||||
on: () => mockReq,
|
||||
write: () => {},
|
||||
end: () => process.nextTick(() => mockReq.errorCallback?.(new Error("Mock error"))),
|
||||
errorCallback: null as any,
|
||||
};
|
||||
|
||||
mockReq.on = function (event: string, callback: any) {
|
||||
if (event === "error") mockReq.errorCallback = callback;
|
||||
return mockReq;
|
||||
};
|
||||
|
||||
return mockReq as any;
|
||||
};
|
||||
|
||||
const agent = new https.Agent({ family: 4 });
|
||||
|
||||
return fetch("https://test.example/", { agent })
|
||||
.catch(() => {}) // Ignore mock error
|
||||
.finally(() => {
|
||||
https.request = originalRequest;
|
||||
|
||||
expect(capturedOptions).not.toBeNull();
|
||||
expect(capturedOptions.agent.options.family).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle IPv6 family value", () => {
|
||||
let capturedOptions: any = null;
|
||||
|
||||
const originalRequest = https.request;
|
||||
https.request = function (...args: any[]) {
|
||||
capturedOptions = args[0];
|
||||
|
||||
const mockReq = {
|
||||
on: () => mockReq,
|
||||
write: () => {},
|
||||
end: () => process.nextTick(() => mockReq.errorCallback?.(new Error("Mock error"))),
|
||||
errorCallback: null as any,
|
||||
};
|
||||
|
||||
mockReq.on = function (event: string, callback: any) {
|
||||
if (event === "error") mockReq.errorCallback = callback;
|
||||
return mockReq;
|
||||
};
|
||||
|
||||
return mockReq as any;
|
||||
};
|
||||
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
|
||||
return fetch("https://test.example/", { agent })
|
||||
.catch(() => {}) // Ignore mock error
|
||||
.finally(() => {
|
||||
https.request = originalRequest;
|
||||
|
||||
expect(capturedOptions).not.toBeNull();
|
||||
expect(capturedOptions.agent.options.family).toBe(6);
|
||||
});
|
||||
});
|
||||
});
|
||||
7
test/js/node/http/package.json
Normal file
7
test/js/node/http/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "node-http-tests",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
}
|
||||
79
test/regression/issue/ipv6-https-agent-family.test.ts
Normal file
79
test/regression/issue/ipv6-https-agent-family.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import https from "node:https";
|
||||
|
||||
describe("IPv6 HTTPS Agent family regression test", () => {
|
||||
test("HTTPS agent should pass family option to DNS lookup", () => {
|
||||
// This is a regression test for the issue where IPv6 family setting
|
||||
// in https.Agent was being ignored during DNS resolution
|
||||
|
||||
let dnsLookupOptions: any = null;
|
||||
|
||||
// Mock lookup to capture the options passed to DNS
|
||||
const mockLookup = (hostname: string, options: any, callback: any) => {
|
||||
dnsLookupOptions = { ...options };
|
||||
// Return mock IPv6 addresses
|
||||
callback(null, [
|
||||
{ address: "2001:db8::1", family: 6 },
|
||||
{ address: "2001:db8::2", family: 6 },
|
||||
]);
|
||||
};
|
||||
|
||||
const httpsAgent = new https.Agent({ family: 6 });
|
||||
|
||||
// Create request similar to the user's code
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "test.example.com",
|
||||
path: "/ip",
|
||||
agent: httpsAgent,
|
||||
lookup: mockLookup,
|
||||
},
|
||||
res => {},
|
||||
);
|
||||
|
||||
req.on("error", err => {
|
||||
// Expected since we're using mock addresses
|
||||
});
|
||||
|
||||
req.end();
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
// Verify that DNS lookup was called with family: 6
|
||||
expect(dnsLookupOptions).not.toBeNull();
|
||||
expect(dnsLookupOptions.family).toBe(6);
|
||||
expect(dnsLookupOptions.all).toBe(true);
|
||||
req.destroy();
|
||||
resolve(undefined);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
test("family option should be ignored when using IP address directly", () => {
|
||||
// When hostname is already an IP, family option should be ignored
|
||||
// but agent should still store the option correctly
|
||||
|
||||
const agent = new https.Agent({ family: 6 });
|
||||
expect(agent.options.family).toBe(6);
|
||||
|
||||
// Using an IP address directly - this should skip DNS lookup entirely
|
||||
// The family option should still be stored in agent but not used for connection
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "127.0.0.1", // IPv4 address
|
||||
path: "/",
|
||||
port: 8443,
|
||||
agent: agent,
|
||||
},
|
||||
res => {},
|
||||
);
|
||||
|
||||
req.on("error", err => {
|
||||
// Expected connection error since no server is listening
|
||||
});
|
||||
|
||||
// This should not throw or crash
|
||||
req.end();
|
||||
req.destroy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user