Compare commits

..

3 Commits

Author SHA1 Message Date
Claude Bot
de81f16c60 Move regression test to test/regression/issue/26597.test.ts
Per CLAUDE.md guidelines, tests for specific GitHub issues should be
placed in test/regression/issue/${issueNumber}.test.ts for consistency
and discoverability.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:50:34 +00:00
Claude Bot
9c595ba596 Merge branch 'main' into claude/fix-parallel-windows-backslash
Resolve conflict in multi_run.zig - keep main's explicit Windows check
(Environment.isWindows and raw_name[0] == '\\') instead of the more
generic std.fs.path.sep approach.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:28:48 +00:00
Claude Bot
92d7c2fe75 fix(cli): quote bun path in parallel file execution on Windows
When running files in parallel with `bun --parallel a.js b.js`, the bun
executable path was being passed unquoted to `bun exec`. On Windows, this
caused the backslashes in paths like `C:\Users\...\bun.exe` to be
interpreted as escape characters by the shell parser, resulting in
malformed paths like `C:Usersjake.bunbinbun.exe` and a "command not
found" error.

The fix quotes the bun path so backslashes are preserved. Also adds
detection for Windows paths starting with backslash in the `is_file`
check.

Fixes #26597

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 06:35:36 +00:00
12 changed files with 96 additions and 350 deletions

View File

@@ -2433,13 +2433,12 @@ declare module "bun" {
type SIMD = "baseline" | "modern";
type CompileTarget =
| `bun-darwin-${Architecture}`
| `bun-darwin-${Architecture}-${SIMD}`
| `bun-darwin-x64-${SIMD}`
| `bun-linux-${Architecture}`
| `bun-linux-${Architecture}-${Libc}`
| `bun-linux-${Architecture}-${SIMD}`
| `bun-linux-${Architecture}-${SIMD}-${Libc}`
| "bun-windows-x64"
| `bun-windows-x64-${SIMD}`;
| `bun-windows-x64-${SIMD}`
| `bun-linux-x64-${SIMD}-${Libc}`;
}
/**
@@ -5679,7 +5678,7 @@ declare module "bun" {
*
* This will apply to all sockets from the same {@link Listener}. it is per socket only for {@link Bun.connect}.
*/
reload(options: Pick<SocketOptions<Data>, "socket">): void;
reload(handler: SocketHandler): void;
/**
* Get the server that created this socket
@@ -6022,7 +6021,7 @@ declare module "bun" {
stop(closeActiveConnections?: boolean): void;
ref(): void;
unref(): void;
reload(options: Pick<SocketOptions<Data>, "socket">): void;
reload(options: Pick<Partial<SocketOptions>, "socket">): void;
data: Data;
}
interface TCPSocketListener<Data = unknown> extends SocketListener<Data> {

View File

@@ -83,7 +83,6 @@
namespace WebCore {
WTF_MAKE_TZONE_ALLOCATED_IMPL(WebSocket);
extern "C" int Bun__getTLSRejectUnauthorizedValue();
extern "C" bool Bun__isNoProxy(const char* hostname, size_t hostname_len, const char* host, size_t host_len);
static ErrorEvent::Init createErrorEventInit(WebSocket& webSocket, const String& reason, JSC::JSGlobalObject* globalObject)
{
@@ -574,19 +573,6 @@ ExceptionOr<void> WebSocket::connect(const String& url, const Vector<String>& pr
// Determine connection type based on proxy usage and TLS requirements
bool hasProxy = proxyConfig.has_value();
// Check NO_PROXY even for explicitly-provided proxies
if (hasProxy) {
auto hostStr = m_url.host().toString();
auto hostWithPort = hostName(m_url, is_secure);
auto hostUtf8 = hostStr.utf8();
auto hostWithPortUtf8 = hostWithPort.utf8();
if (Bun__isNoProxy(hostUtf8.data(), hostUtf8.length(), hostWithPortUtf8.data(), hostWithPortUtf8.length())) {
proxyConfig = std::nullopt;
hasProxy = false;
}
}
bool proxyIsHTTPS = hasProxy && proxyConfig->isHTTPS;
// Connection type determines what kind of socket we use:

View File

@@ -158,13 +158,6 @@ export fn Bun__getTLSRejectUnauthorizedValue() i32 {
return if (jsc.VirtualMachine.get().getTLSRejectUnauthorized()) 1 else 0;
}
export fn Bun__isNoProxy(hostname_ptr: [*]const u8, hostname_len: usize, host_ptr: [*]const u8, host_len: usize) bool {
const vm = jsc.VirtualMachine.get();
const hostname: ?[]const u8 = if (hostname_len > 0) hostname_ptr[0..hostname_len] else null;
const host: ?[]const u8 = if (host_len > 0) host_ptr[0..host_len] else null;
return vm.transpiler.env.isNoProxy(hostname, host);
}
export fn Bun__setVerboseFetchValue(value: i32) void {
VirtualMachine.get().default_verbose_fetch = if (value == 1) .headers else if (value == 2) .curl else .none;
}

View File

@@ -1036,14 +1036,9 @@ pub const FetchTasklet = struct {
var proxy: ?ZigURL = null;
if (fetch_options.proxy) |proxy_opt| {
if (!proxy_opt.isEmpty()) { //if is empty just ignore proxy
// Check NO_PROXY even for explicitly-provided proxies
if (!jsc_vm.transpiler.env.isNoProxy(fetch_options.url.hostname, fetch_options.url.host)) {
proxy = proxy_opt;
}
proxy = fetch_options.proxy orelse jsc_vm.transpiler.env.getHttpProxyFor(fetch_options.url);
}
// else: proxy: "" means explicitly no proxy (direct connection)
} else {
// no proxy provided, use default proxy resolution
proxy = jsc_vm.transpiler.env.getHttpProxyFor(fetch_options.url);
}

View File

@@ -184,88 +184,71 @@ pub const Loader = struct {
}
}
// NO_PROXY filter
// See the syntax at https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/
if (http_proxy != null and hostname != null) {
if (this.isNoProxy(hostname, host)) {
return null;
if (this.get("no_proxy") orelse this.get("NO_PROXY")) |no_proxy_text| {
if (no_proxy_text.len == 0 or strings.eqlComptime(no_proxy_text, "\"\"") or strings.eqlComptime(no_proxy_text, "''")) {
return http_proxy;
}
var no_proxy_iter = std.mem.splitScalar(u8, no_proxy_text, ',');
while (no_proxy_iter.next()) |no_proxy_item| {
var no_proxy_entry = strings.trim(no_proxy_item, &strings.whitespace_chars);
if (no_proxy_entry.len == 0) {
continue;
}
if (strings.eql(no_proxy_entry, "*")) {
return null;
}
//strips .
if (strings.startsWithChar(no_proxy_entry, '.')) {
no_proxy_entry = no_proxy_entry[1..];
if (no_proxy_entry.len == 0) {
continue;
}
}
// Determine if entry contains a port or is an IPv6 address
// IPv6 addresses contain multiple colons (e.g., "::1", "2001:db8::1")
// Bracketed IPv6 with port: "[::1]:8080"
// Host with port: "localhost:8080" (single colon)
const colon_count = std.mem.count(u8, no_proxy_entry, ":");
const is_bracketed_ipv6 = strings.startsWithChar(no_proxy_entry, '[');
const has_port = blk: {
if (is_bracketed_ipv6) {
// Bracketed IPv6: check for "]:port" pattern
if (std.mem.indexOf(u8, no_proxy_entry, "]:")) |_| {
break :blk true;
}
break :blk false;
} else if (colon_count == 1) {
// Single colon means host:port (not IPv6)
break :blk true;
}
// Multiple colons without brackets = bare IPv6 literal (no port)
break :blk false;
};
if (has_port) {
// Entry has a port, do exact match against host:port
if (host) |h| {
if (strings.eqlCaseInsensitiveASCII(h, no_proxy_entry, true)) {
return null;
}
}
} else {
// Entry is hostname/IPv6 only, match against hostname (suffix match)
if (strings.endsWith(hostname.?, no_proxy_entry)) {
return null;
}
}
}
}
}
return http_proxy;
}
/// Returns true if the given hostname/host should bypass the proxy
/// according to the NO_PROXY / no_proxy environment variable.
pub fn isNoProxy(this: *const Loader, hostname: ?[]const u8, host: ?[]const u8) bool {
// NO_PROXY filter
// See the syntax at https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/
const hn = hostname orelse return false;
const no_proxy_text = this.get("no_proxy") orelse this.get("NO_PROXY") orelse return false;
if (no_proxy_text.len == 0 or strings.eqlComptime(no_proxy_text, "\"\"") or strings.eqlComptime(no_proxy_text, "''")) {
return false;
}
var no_proxy_iter = std.mem.splitScalar(u8, no_proxy_text, ',');
while (no_proxy_iter.next()) |no_proxy_item| {
var no_proxy_entry = strings.trim(no_proxy_item, &strings.whitespace_chars);
if (no_proxy_entry.len == 0) {
continue;
}
if (strings.eql(no_proxy_entry, "*")) {
return true;
}
//strips .
if (strings.startsWithChar(no_proxy_entry, '.')) {
no_proxy_entry = no_proxy_entry[1..];
if (no_proxy_entry.len == 0) {
continue;
}
}
// Determine if entry contains a port or is an IPv6 address
// IPv6 addresses contain multiple colons (e.g., "::1", "2001:db8::1")
// Bracketed IPv6 with port: "[::1]:8080"
// Host with port: "localhost:8080" (single colon)
const colon_count = std.mem.count(u8, no_proxy_entry, ":");
const is_bracketed_ipv6 = strings.startsWithChar(no_proxy_entry, '[');
const has_port = blk: {
if (is_bracketed_ipv6) {
// Bracketed IPv6: check for "]:port" pattern
if (std.mem.indexOf(u8, no_proxy_entry, "]:")) |_| {
break :blk true;
}
break :blk false;
} else if (colon_count == 1) {
// Single colon means host:port (not IPv6)
break :blk true;
}
// Multiple colons without brackets = bare IPv6 literal (no port)
break :blk false;
};
if (has_port) {
// Entry has a port, do exact match against host:port
if (host) |h| {
if (strings.eqlCaseInsensitiveASCII(h, no_proxy_entry, true)) {
return true;
}
}
} else {
// Entry is hostname/IPv6 only, match exact or dot-boundary suffix (case-insensitive)
const entry_len = no_proxy_entry.len;
if (hn.len == entry_len) {
if (strings.eqlCaseInsensitiveASCII(hn, no_proxy_entry, true)) return true;
} else if (hn.len > entry_len and
hn[hn.len - entry_len - 1] == '.' and
strings.eqlCaseInsensitiveASCII(hn[hn.len - entry_len ..], no_proxy_entry, true))
{
return true;
}
}
}
return false;
}
var did_load_ccache_path: bool = false;
pub fn loadCCachePath(this: *Loader, fs: *Fs.FileSystem) void {

View File

@@ -464,7 +464,7 @@ pub fn createInstance(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFra
ptr.#connection.setSocket(.{
.SocketTCP = uws.SocketTCP.connectUnixAnon(path, ctx, ptr, false) catch |err| {
ptr.deref();
return globalObject.throwError(err, "failed to connect to mysql");
return globalObject.throwError(err, "failed to connect to postgresql");
},
});
} else {

View File

@@ -1,21 +1,10 @@
import { expectAssignable, expectType } from "./utilities";
import { expectType } from "./utilities";
Bun.build({
entrypoints: ["hey"],
splitting: false,
});
// Build.CompileTarget should accept SIMD variants (issue #26247)
expectAssignable<Bun.Build.CompileTarget>("bun-linux-x64-modern");
expectAssignable<Bun.Build.CompileTarget>("bun-linux-x64-baseline");
expectAssignable<Bun.Build.CompileTarget>("bun-linux-arm64-modern");
expectAssignable<Bun.Build.CompileTarget>("bun-linux-arm64-baseline");
expectAssignable<Bun.Build.CompileTarget>("bun-linux-x64-modern-glibc");
expectAssignable<Bun.Build.CompileTarget>("bun-linux-x64-modern-musl");
expectAssignable<Bun.Build.CompileTarget>("bun-darwin-x64-modern");
expectAssignable<Bun.Build.CompileTarget>("bun-darwin-arm64-baseline");
expectAssignable<Bun.Build.CompileTarget>("bun-windows-x64-modern");
Bun.build({
entrypoints: ["hey"],
splitting: false,

View File

@@ -145,23 +145,3 @@ listener.reload({
// ...listener.
},
});
// Test Socket.reload() type signature (issue #26290)
// The socket instance's reload() method should also accept { socket: handler }
await Bun.connect({
data: { arg: "asdf" },
socket: {
open(socket) {
// Socket.reload() should accept { socket: handler }, not handler directly
socket.reload({
socket: {
open() {},
data() {},
},
});
},
data() {},
},
hostname: "localhost",
port: 1,
});

View File

@@ -1,7 +1,7 @@
import axios from "axios";
import type { Server } from "bun";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tls as tlsCert } from "harness";
import { tls as tlsCert } from "harness";
import { HttpsProxyAgent } from "https-proxy-agent";
import { once } from "node:events";
import net from "node:net";
@@ -859,84 +859,3 @@ 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);
});
});

View File

@@ -13,8 +13,6 @@ 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
@@ -658,86 +656,3 @@ 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);
});
});

View File

@@ -0,0 +1,28 @@
// https://github.com/oven-sh/bun/issues/26597
// Parallel file execution should work on Windows when bun path contains backslashes
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("#26597: parallel file execution works (Windows backslash path handling)", async () => {
using dir = tempDir("issue-26597", {
"a.js": "console.log('hello-a')",
"b.js": "console.log('hello-b')",
});
// This previously failed on Windows with "bun: command not found: C:Usersjake.bunbinbun.exe"
// because backslashes in the bun executable path were being stripped by the shell parser.
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--parallel", "a.js", "b.js"],
env: { ...bunEnv, NO_COLOR: "1" },
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Both scripts should run and produce prefixed output
expect(stdout).toMatch(/a\.js\s+\| .*hello-a/);
expect(stdout).toMatch(/b\.js\s+\| .*hello-b/);
expect(exitCode).toBe(0);
});

View File

@@ -1,41 +0,0 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("MySQL unix socket error message should not say 'postgresql'", async () => {
using dir = tempDir("mysql-socket-test", {
"test.ts": `
import { join } from "path";
// Create a regular file (not a socket) to trigger the connection error
const fakeSockPath = join(import.meta.dirname, "fake.sock");
await Bun.write(fakeSockPath, "");
const conn = new Bun.SQL({
adapter: 'mysql',
path: fakeSockPath,
username: 'root',
database: 'test'
});
try {
await conn\`select 1\`;
} catch(e) {
console.log(e.message);
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const output = stdout + stderr;
expect(output).not.toContain("postgresql");
expect(output).toContain("mysql");
expect(exitCode).toBe(0);
});