Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/js/builtins/ProcessObjectInternals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,9 +706,7 @@ export function loadEnvFile(path) {
const parsed = require("node:util").parseEnv(content);
const env = process.env;
for (const key of Object.keys(parsed)) {
// Node: existing env keys win; the file only fills in missing ones.
// Compare against undefined rather than `in`: accessor-backed keys (TZ,
// HTTP_PROXY) always exist but read back undefined while unset.
// Node: existing env keys win. Not `in`: an accessor-backed key such as TZ exists while unset and reads undefined.
if (env[key] === undefined) {
env[key] = parsed[key];
}
Expand Down
4 changes: 1 addition & 3 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2008,9 +2008,7 @@ JSC_DEFINE_HOST_FUNCTION_WITH_ATTRIBUTES(Process_functionExecve, __attribute__((
for (unsigned i = 0; i < envNames.size(); i++) {
JSValue value = envObject->get(globalObject, envNames[i]);
RETURN_IF_EXCEPTION(scope, {});
// The accessor-backed keys (TZ, NODE_TLS_REJECT_UNAUTHORIZED,
// BUN_CONFIG_VERBOSE_FETCH) read back undefined for an unset /
// empty value; skip rather than rejecting the defaulted env.
// process.env reads a non-UTF-8 name, and `$NAME` while NAME is not set, as undefined: skip it, do not reject the defaulted env.
if (value.isUndefined())
continue;
const WTF::String& keyStr = envNames[i].string();
Expand Down
6 changes: 3 additions & 3 deletions src/jsc/bindings/JSEnvironmentVariableMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ JSC_DEFINE_CUSTOM_GETTER(jsTimeZoneEnvironmentVariableGetter, (JSGlobalObject *
return JSValue::encode(hasExistingValue);
}

if (!Bun__getEnvValue(globalObject, &name, &value) || value.len == 0) {
if (!Bun__getEnvValue(globalObject, &name, &value)) {
return JSValue::encode(jsUndefined());
}

Expand Down Expand Up @@ -391,7 +391,7 @@ JSC_DEFINE_CUSTOM_GETTER(jsNodeTLSRejectUnauthorizedGetter, (JSGlobalObject * gl
EncodedSlice name = toEncodedSlice(propertyName.publicName());
EncodedSlice value = { nullptr, 0 };

if (!Bun__getEnvValue(globalObject, &name, &value) || value.len == 0) {
if (!Bun__getEnvValue(globalObject, &name, &value)) {
return JSValue::encode(jsUndefined());
}

Expand Down Expand Up @@ -439,7 +439,7 @@ JSC_DEFINE_CUSTOM_GETTER(jsBunConfigVerboseFetchGetter, (JSGlobalObject * global
EncodedSlice name = toEncodedSlice(propertyName.publicName());
EncodedSlice value = { nullptr, 0 };

if (!Bun__getEnvValue(globalObject, &name, &value) || value.len == 0) {
if (!Bun__getEnvValue(globalObject, &name, &value)) {
return JSValue::encode(jsUndefined());
}

Expand Down
62 changes: 58 additions & 4 deletions test/js/node/process/process-execve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,7 @@ describe.concurrent("process.execve", () => {
});

test.skipIf(isWindows)("inherits process.env when env is omitted with an empty TZ in the OS env", async () => {
// The TZ / NODE_TLS_REJECT_UNAUTHORIZED / BUN_CONFIG_VERBOSE_FETCH
// accessors read back undefined for an empty value; the execve env loop
// must skip those rather than rejecting the defaulted process.env with
// ERR_INVALID_ARG_VALUE naming an argument the caller never passed.
// An empty TZ is a valid value: execve must not reject the defaulted process.env for it.
await using proc = Bun.spawn({
cmd: [
bunExe(),
Expand All @@ -322,6 +319,63 @@ describe.concurrent("process.execve", () => {
});
});

test.skipIf(isWindows)(
"inherits process.env when env is omitted with a `$`-prefixed name in the OS env",
async () => {
// process.env reads `$EXECVE_DOLLAR` as undefined while EXECVE_DOLLAR is not set: execve must skip it, not reject the defaulted env.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`try { process.execve("/definitely/does/not/exist", ["x"]); }
catch (e) { console.log(e.code); }`,
],
env: { ...bunEnv, EXECVE_DOLLAR: undefined, $EXECVE_DOLLAR: "1" },
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect({ stdout: stdout.trim(), stderr: exitCode === 0 ? "" : stderr, exitCode }).toEqual({
stdout: "ENOENT",
stderr: "",
exitCode: 0,
});
},
);

// https://github.com/oven-sh/bun/issues/44379
test.skipIf(isWindows).each([
["env is omitted", ""],
["env is process.env", ", process.env"],
["env is a spread of process.env", ", { ...process.env }"],
])(
"passes an empty TZ, NODE_TLS_REJECT_UNAUTHORIZED and BUN_CONFIG_VERBOSE_FETCH to the new image when %s",
async (_, envArgument) => {
const names = ["TZ", "NODE_TLS_REJECT_UNAUTHORIZED", "BUN_CONFIG_VERBOSE_FETCH"];
const print = `console.log(JSON.stringify(${JSON.stringify(names)}.map(name => process.env[name] ?? null)))`;
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`process.execve(process.execPath, [process.execPath, "-e", ${JSON.stringify(print)}]${envArgument});`,
],
env: { ...bunEnv, ...Object.fromEntries(names.map(name => [name, ""])) },
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect({ stdout: stdout.trim(), stderr: exitCode === 0 ? "" : stderr, exitCode }).toEqual({
stdout: '["","",""]',
stderr: "",
exitCode: 0,
});
},
);

test.skipIf(isWindows)("validates arguments", async () => {
await using proc = Bun.spawn({
cmd: [
Expand Down
96 changes: 96 additions & 0 deletions test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,102 @@ it("process.loadEnvFile can set accessor-backed keys and respects empty values",
expect({ stdout, exitCode }).toEqual({ stdout: 'http://from-dotenv:8080\n""\nfresh\n', exitCode: 0 });
});

// https://github.com/oven-sh/bun/issues/44379
describe.concurrent("TZ, NODE_TLS_REJECT_UNAUTHORIZED and BUN_CONFIG_VERBOSE_FETCH set to an empty string", () => {
const names = ["TZ", "NODE_TLS_REJECT_UNAUTHORIZED", "BUN_CONFIG_VERBOSE_FETCH"];
const setTo = value => Object.fromEntries(names.map(name => [name, value]));

async function run(files, env) {
using dir = tempDir("process-env-empty", files);
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: { ...bunEnv, ...env },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { result: stdout ? JSON.parse(stdout) : stderr, exitCode };
}

const read = `
const result = {};
for (const name of ${JSON.stringify(names)}) {
result[name] = {
read: process.env[name],
bunEnv: Bun.env[name],
importMetaEnv: import.meta.env[name],
listed: Object.keys(process.env).includes(name),
spread: { ...process.env }[name],
json: JSON.parse(JSON.stringify(process.env))[name],
};
}
console.log(JSON.stringify(result));
`;
const empty = { read: "", bunEnv: "", importMetaEnv: "", listed: true, spread: "", json: "" };

it.each([
["the environment", { "index.js": read }, setTo("")],
[".env", { ".env": names.map(name => `${name}=\n`).join(""), "index.js": read }, setTo(undefined)],
])("process.env reads them from %s", async (_, files, env) => {
expect(await run(files, env)).toEqual({ result: setTo(empty), exitCode: 0 });
});

const load = {
"vars.env": "TZ=Asia/Tokyo\nNODE_TLS_REJECT_UNAUTHORIZED=0\nBUN_CONFIG_VERBOSE_FETCH=curl\n",
"index.js": `
const offset = () => new Date("2024-07-15T12:00:00Z").getTimezoneOffset();
const before = offset();
process.loadEnvFile("vars.env");
console.log(JSON.stringify({
values: ${JSON.stringify(names)}.map(name => process.env[name]),
timeZoneChanged: offset() !== before,
offset: offset(),
}));
`,
};

it("process.loadEnvFile keeps them", async () => {
const { result, exitCode } = await run(load, setTo(""));
expect({ values: result.values, timeZoneChanged: result.timeZoneChanged, exitCode }).toEqual({
values: ["", "", ""],
timeZoneChanged: false,
exitCode: 0,
});
});

it("process.loadEnvFile fills them in when they are not set", async () => {
const { result, exitCode } = await run(load, setTo(undefined));
// Asia/Tokyo is UTC+9 all year.
expect({ values: result.values, offset: result.offset, exitCode }).toEqual({
values: ["Asia/Tokyo", "0", "curl"],
offset: -540,
exitCode: 0,
});
});

it("child processes inherit them", async () => {
const print = `console.log(JSON.stringify(${JSON.stringify(names)}.map(name => process.env[name] ?? null)))`;
const children = {
"index.js": `
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const cmd = [process.execPath, "-e", ${JSON.stringify(print)}];
const [childProcess, bunSpawn, shell] = await Promise.all([
promisify(execFile)(cmd[0], cmd.slice(1)).then(({ stdout }) => JSON.parse(stdout)),
Bun.spawn({ cmd, env: process.env, stdout: "pipe" }).stdout.json(),
Bun.$\`\${cmd}\`.json(),
]);
console.log(JSON.stringify({ childProcess, bunSpawn, shell }));
`,
};
expect(await run(children, setTo(""))).toEqual({
result: { childProcess: ["", "", ""], bunSpawn: ["", "", ""], shell: ["", "", ""] },
exitCode: 0,
});
});
});

it("process.env defineProperty matches assignment semantics", () => {
// Node's EnvDefiner delegates to the env setter after validating the
// descriptor: symbol keys throw a TypeError...
Expand Down
48 changes: 48 additions & 0 deletions test/js/node/worker_threads/worker_threads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1761,6 +1761,54 @@ test("env: process.env reads in a worker module are evaluated at runtime against
});
});

// https://github.com/oven-sh/bun/issues/44379
test.concurrent.each([
"default env",
"default env, after the main thread read process.env",
"env: process.env",
"env: SHARE_ENV",
])(
"env: a worker reads an empty TZ, NODE_TLS_REJECT_UNAUTHORIZED and BUN_CONFIG_VERBOSE_FETCH as an empty string (%s)",
async mode => {
using dir = tempDir("worker-threads-env-empty", {
"main.js": `
const { Worker, SHARE_ENV, isMainThread, parentPort } = require("node:worker_threads");
const read = () =>
["TZ", "NODE_TLS_REJECT_UNAUTHORIZED", "BUN_CONFIG_VERBOSE_FETCH"].map(name => process.env[name] ?? null);
if (isMainThread) {
const mode = process.argv[2];
if (mode === "default env, after the main thread read process.env") read();
const worker = new Worker(
__filename,
mode === "env: process.env" ? { env: process.env } : mode === "env: SHARE_ENV" ? { env: SHARE_ENV } : {},
);
worker.on("error", err => { console.error(err); process.exit(1); });
worker.on("message", fromWorker => console.log(JSON.stringify({ worker: fromWorker, main: read() })));
} else {
parentPort.postMessage(read());
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "main.js", mode],
env: { ...bunEnv, TZ: "", NODE_TLS_REJECT_UNAUTHORIZED: "", BUN_CONFIG_VERBOSE_FETCH: "" },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({
message: stdout ? JSON.parse(stdout) : stdout,
stderr: exitCode === 0 ? "" : stderr,
exitCode,
}).toEqual({
message: { worker: ["", "", ""], main: ["", "", ""] },
stderr: "",
exitCode: 0,
});
},
);

describe("env: SHARE_ENV shares the spawning thread's env, not a process-wide one", () => {
async function run(mode: string) {
const proc = Bun.spawn({
Expand Down
Loading