mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(proxy): respect NO_PROXY for explicit proxy options in fetch and ws (#26608)
### What does this PR do? Extract NO_PROXY checking logic from getHttpProxyFor into a reusable isNoProxy method on the env Loader. This allows both fetch() and WebSocket to check NO_PROXY even when a proxy is explicitly provided via the proxy option (not just via http_proxy env var). Changes: - env_loader.zig: Extract isNoProxy() from getHttpProxyFor() - FetchTasklet.zig: Check isNoProxy() before using explicit proxy - WebSocket.cpp: Check Bun__isNoProxy() before using explicit proxy - virtual_machine_exports.zig: Export Bun__isNoProxy for C++ access - Add NO_PROXY tests for both fetch and WebSocket proxy paths ### How did you verify your code works? Tests --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import axios from "axios";
|
||||
import type { Server } from "bun";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { tls as tlsCert } from "harness";
|
||||
import { bunEnv, bunExe, tls as tlsCert } from "harness";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { once } from "node:events";
|
||||
import net from "node:net";
|
||||
@@ -859,3 +859,84 @@ describe("proxy object format with headers", () => {
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe.concurrent("NO_PROXY with explicit proxy option", () => {
|
||||
// These tests use subprocess spawning because NO_PROXY is read from the
|
||||
// process environment at startup. A dead proxy that immediately closes
|
||||
// connections is used so that if NO_PROXY doesn't work, the fetch fails
|
||||
// with a connection error.
|
||||
let deadProxyPort: number;
|
||||
let deadProxy: ReturnType<typeof Bun.listen>;
|
||||
|
||||
beforeAll(() => {
|
||||
deadProxy = Bun.listen({
|
||||
hostname: "127.0.0.1",
|
||||
port: 0,
|
||||
socket: {
|
||||
open(socket) {
|
||||
socket.end();
|
||||
},
|
||||
data() {},
|
||||
},
|
||||
});
|
||||
deadProxyPort = deadProxy.port;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
deadProxy.stop(true);
|
||||
});
|
||||
|
||||
test("NO_PROXY bypasses explicit proxy for fetch", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`const resp = await fetch("http://localhost:${httpServer.port}", { proxy: "http://127.0.0.1:${deadProxyPort}" }); console.log(resp.status);`,
|
||||
],
|
||||
env: { ...bunEnv, NO_PROXY: "localhost" },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
if (exitCode !== 0) console.error("stderr:", stderr);
|
||||
expect(stdout.trim()).toBe("200");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("NO_PROXY with port bypasses explicit proxy for fetch", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`const resp = await fetch("http://localhost:${httpServer.port}", { proxy: "http://127.0.0.1:${deadProxyPort}" }); console.log(resp.status);`,
|
||||
],
|
||||
env: { ...bunEnv, NO_PROXY: `localhost:${httpServer.port}` },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
if (exitCode !== 0) console.error("stderr:", stderr);
|
||||
expect(stdout.trim()).toBe("200");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("NO_PROXY non-match does not bypass explicit proxy", async () => {
|
||||
// NO_PROXY doesn't match, so fetch should try the dead proxy and fail
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`try { await fetch("http://localhost:${httpServer.port}", { proxy: "http://127.0.0.1:${deadProxyPort}" }); process.exit(1); } catch { process.exit(0); }`,
|
||||
],
|
||||
env: { ...bunEnv, NO_PROXY: "other.com" },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
// exit(0) means fetch threw (proxy connection failed), proving proxy was used
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ const { HttpsProxyAgent } = require("https-proxy-agent") as {
|
||||
// Use docker-compose infrastructure for squid proxy
|
||||
|
||||
const gc = harness.gc;
|
||||
const bunExe = harness.bunExe;
|
||||
const bunEnv = harness.bunEnv;
|
||||
const isDockerEnabled = harness.isDockerEnabled;
|
||||
|
||||
// HTTP CONNECT proxy server for WebSocket tunneling
|
||||
@@ -656,3 +658,86 @@ describe("ws module with HttpsProxyAgent", () => {
|
||||
gc();
|
||||
});
|
||||
});
|
||||
|
||||
describe.concurrent("WebSocket NO_PROXY bypass", () => {
|
||||
test("NO_PROXY matching hostname bypasses explicit proxy for ws://", async () => {
|
||||
// authProxy requires credentials; if NO_PROXY works, the WebSocket bypasses
|
||||
// the proxy and connects directly. If NO_PROXY doesn't work, the proxy
|
||||
// rejects with 407 and the WebSocket errors.
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`const ws = new WebSocket("ws://127.0.0.1:${wsPort}", { proxy: "http://127.0.0.1:${authProxyPort}" });
|
||||
ws.onopen = () => { ws.close(); process.exit(0); };
|
||||
ws.onerror = () => { process.exit(1); };`,
|
||||
],
|
||||
env: { ...bunEnv, NO_PROXY: "127.0.0.1" },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
if (exitCode !== 0) console.error("stderr:", stderr);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("NO_PROXY matching host:port bypasses proxy for ws://", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`const ws = new WebSocket("ws://127.0.0.1:${wsPort}", { proxy: "http://127.0.0.1:${authProxyPort}" });
|
||||
ws.onopen = () => { ws.close(); process.exit(0); };
|
||||
ws.onerror = () => { process.exit(1); };`,
|
||||
],
|
||||
env: { ...bunEnv, NO_PROXY: `127.0.0.1:${wsPort}` },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
if (exitCode !== 0) console.error("stderr:", stderr);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("NO_PROXY not matching still uses proxy (auth fails)", async () => {
|
||||
// NO_PROXY doesn't match the target, so the WebSocket should go through
|
||||
// the auth proxy without credentials, which rejects with 407.
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`const ws = new WebSocket("ws://127.0.0.1:${wsPort}", { proxy: "http://127.0.0.1:${authProxyPort}" });
|
||||
ws.onopen = () => { process.exit(1); };
|
||||
ws.onerror = () => { process.exit(0); };`,
|
||||
],
|
||||
env: { ...bunEnv, NO_PROXY: "other.host.com" },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
// exit(0) means onerror fired, proving the proxy was used (and auth failed)
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("NO_PROXY=* bypasses all proxies", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`const ws = new WebSocket("ws://127.0.0.1:${wsPort}", { proxy: "http://127.0.0.1:${authProxyPort}" });
|
||||
ws.onopen = () => { ws.close(); process.exit(0); };
|
||||
ws.onerror = () => { process.exit(1); };`,
|
||||
],
|
||||
env: { ...bunEnv, NO_PROXY: "*" },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
if (exitCode !== 0) console.error("stderr:", stderr);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user