mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
5 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b848af6ef | ||
|
|
a14a89ca95 | ||
|
|
a5246344fa | ||
|
|
f648483fe7 | ||
|
|
01fa61045f |
11
packages/bun-types/bun.d.ts
vendored
11
packages/bun-types/bun.d.ts
vendored
@@ -2433,12 +2433,13 @@ declare module "bun" {
|
||||
type SIMD = "baseline" | "modern";
|
||||
type CompileTarget =
|
||||
| `bun-darwin-${Architecture}`
|
||||
| `bun-darwin-x64-${SIMD}`
|
||||
| `bun-darwin-${Architecture}-${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-linux-x64-${SIMD}-${Libc}`;
|
||||
| `bun-windows-x64-${SIMD}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5678,7 +5679,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(handler: SocketHandler): void;
|
||||
reload(options: Pick<SocketOptions<Data>, "socket">): void;
|
||||
|
||||
/**
|
||||
* Get the server that created this socket
|
||||
@@ -6021,7 +6022,7 @@ declare module "bun" {
|
||||
stop(closeActiveConnections?: boolean): void;
|
||||
ref(): void;
|
||||
unref(): void;
|
||||
reload(options: Pick<Partial<SocketOptions>, "socket">): void;
|
||||
reload(options: Pick<SocketOptions<Data>, "socket">): void;
|
||||
data: Data;
|
||||
}
|
||||
interface TCPSocketListener<Data = unknown> extends SocketListener<Data> {
|
||||
|
||||
@@ -394,6 +394,22 @@ extern "C" void bun_restore_stdio()
|
||||
|
||||
#if !OS(WINDOWS)
|
||||
|
||||
// Restore cursor visibility on TTY before exiting.
|
||||
// Many CLI applications (like Ink) hide the cursor during operation and rely on
|
||||
// cleanup handlers to restore it. If the process exits before the cursor-show
|
||||
// escape sequence is flushed, the cursor remains invisible in the terminal.
|
||||
// Writing the cursor-show sequence here ensures the cursor is always visible
|
||||
// after Bun exits, regardless of how the process terminates.
|
||||
// See: https://github.com/oven-sh/bun/issues/26642
|
||||
if (bun_stdio_tty[STDOUT_FILENO]) {
|
||||
// Show cursor: CSI ? 25 h
|
||||
const char show_cursor[] = "\x1b[?25h";
|
||||
ssize_t ret;
|
||||
do {
|
||||
ret = write(STDOUT_FILENO, show_cursor, sizeof(show_cursor) - 1);
|
||||
} while (ret == -1 && errno == EINTR);
|
||||
}
|
||||
|
||||
// restore stdio
|
||||
for (int32_t fd = 0; fd < 3; fd++) {
|
||||
if (!bun_stdio_tty[fd])
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
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)
|
||||
{
|
||||
@@ -573,6 +574,19 @@ 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:
|
||||
|
||||
@@ -158,6 +158,13 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1036,9 +1036,14 @@ pub const FetchTasklet = struct {
|
||||
var proxy: ?ZigURL = null;
|
||||
if (fetch_options.proxy) |proxy_opt| {
|
||||
if (!proxy_opt.isEmpty()) { //if is empty just ignore proxy
|
||||
proxy = fetch_options.proxy orelse jsc_vm.transpiler.env.getHttpProxyFor(fetch_options.url);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -184,71 +184,88 @@ 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.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isNoProxy(hostname, host)) {
|
||||
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 {
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { expectType } from "./utilities";
|
||||
import { expectAssignable, 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,
|
||||
|
||||
@@ -145,3 +145,23 @@ 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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
41
test/regression/issue/26642.test.ts
Normal file
41
test/regression/issue/26642.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isMacOS, isWindows } from "harness";
|
||||
|
||||
// Test that cursor visibility is restored on process exit when stdout is a TTY.
|
||||
// This is needed because CLI applications like Ink hide the cursor during operation
|
||||
// and rely on cleanup handlers to restore it. If the process exits before the
|
||||
// cursor-show escape sequence is flushed, the cursor remains invisible.
|
||||
// See: https://github.com/oven-sh/bun/issues/26642
|
||||
|
||||
// Skip on Windows and non-macOS - the script command behavior varies and CI
|
||||
// environments often don't provide proper PTY support. The actual fix is
|
||||
// most critical for macOS terminals where users reported the issue.
|
||||
test.skipIf(isWindows || !isMacOS)("cursor visibility is restored on exit when stdout is TTY", async () => {
|
||||
// Check if script command is available (needed to create a PTY)
|
||||
const hasScript = Bun.which("script");
|
||||
if (!hasScript) {
|
||||
console.log("Skipping test: requires 'script' command for PTY simulation");
|
||||
return;
|
||||
}
|
||||
|
||||
// Script that just exits immediately - cursor restore should happen automatically
|
||||
const testScript = `process.exit(0);`;
|
||||
|
||||
// Use script command to provide a PTY environment (macOS syntax)
|
||||
const scriptCmd = ["script", "-q", "/dev/null", bunExe(), "-e", testScript];
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: scriptCmd,
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// The cursor-show escape sequence is \x1b[?25h
|
||||
// It should be present in stdout when running in a TTY
|
||||
const cursorShow = "\x1b[?25h";
|
||||
expect(stdout).toContain(cursorShow);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user