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
2 changes: 1 addition & 1 deletion docs/runtime/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ Bun reads these environment variables to configure aspects of its behavior.
| `TMPDIR` | Bun occasionally requires a directory to store intermediate assets during bundling or other operations. If unset, defaults to the platform-specific temporary directory: `/tmp` on Linux, `/private/tmp` on macOS. |
| `NO_COLOR` | If `NO_COLOR=1`, then ANSI color output is [disabled](https://no-color.org/). |
| `FORCE_COLOR` | If `FORCE_COLOR=1`, then ANSI color output is forced on, even if `NO_COLOR` is set. |
| `BUN_CONFIG_MAX_HTTP_REQUESTS` | Sets the maximum number of concurrent HTTP requests sent by fetch and `bun install`. Defaults to `256`. Lower it if you run into rate limits or connection issues. |
| `BUN_CONFIG_MAX_HTTP_REQUESTS` | Sets the maximum number of concurrent HTTP requests sent by `fetch` and `bun install`. `fetch` defaults to `256`. `bun install` defaults to `64`, and its `--network-concurrency` flag overrides this variable. Lower it if you run into rate limits or connection issues. |
| `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD` | If `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD=true`, then `bun --watch` does not clear the console on reload |
| `DO_NOT_TRACK` | Disable uploading crash reports to `bun.report` on crash. On macOS & Windows, crash report uploads are enabled by default. Bun sends no other telemetry, though we plan to add some. If `DO_NOT_TRACK=1`, then auto-uploading crash reports and telemetry are both [disabled](https://do-not-track.dev/). |
| `BUN_OPTIONS` | Prepends command-line arguments to any Bun execution. For example, `BUN_OPTIONS="--hot"` makes `bun run dev` behave like `bun --hot run dev`. |
Expand Down
31 changes: 15 additions & 16 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2215,6 +2215,16 @@ pub fn init(
}
}

// `options.load` applies BUN_CONFIG_MAX_HTTP_REQUESTS on top of this default.
http::async_http::MAX_SIMULTANEOUS_REQUESTS.store(
if env.has_http_proxy() {
DEFAULT_MAX_SIMULTANEOUS_REQUESTS_FOR_BUN_INSTALL_FOR_PROXIES
} else {
DEFAULT_MAX_SIMULTANEOUS_REQUESTS_FOR_BUN_INSTALL
},
Ordering::Relaxed,
);

manager.options.load(
// SAFETY: ctx.log is the process-lifetime CLI log set by
// create_context_data(); single-threaded init region.
Expand All @@ -2225,6 +2235,11 @@ pub fn init(
subcommand,
)?;

if let Some(network_concurrency) = cli_network_concurrency {
http::async_http::MAX_SIMULTANEOUS_REQUESTS
.store(usize::from(network_concurrency.max(1)), Ordering::Relaxed);
}

if let Some(config) = ctx.install.as_deref_mut() {
if let Some(p) = config.public_hoist_pattern.take() {
manager.options.public_hoist_pattern = Some(p);
Expand Down Expand Up @@ -2273,22 +2288,6 @@ pub fn init(
}
}

http::async_http::MAX_SIMULTANEOUS_REQUESTS.store(
'brk: {
if let Some(network_concurrency) = cli_network_concurrency {
break 'brk network_concurrency.max(1) as usize;
}

// If any HTTP proxy is set, use a diferent limit
if env.has_http_proxy() {
break 'brk DEFAULT_MAX_SIMULTANEOUS_REQUESTS_FOR_BUN_INSTALL_FOR_PROXIES;
}

DEFAULT_MAX_SIMULTANEOUS_REQUESTS_FOR_BUN_INSTALL
},
Ordering::Relaxed, // .monotonic
);

// `InitOpts.ca: Vec<*const c_void>` (erased `[*:0]const u8`). The HTTP
// thread reads these asynchronously after `init` returns, so park the
// owning `ZBox`es in `holder::CA` for process lifetime (never freed)
Expand Down
118 changes: 35 additions & 83 deletions test/cli/install/bun-install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,105 +153,57 @@ describe.concurrent("bun-install", () => {
});
}

it("bun install --network-concurrency=5 doesnt go over 5 concurrent requests", async () => {
it.each([
{ label: "--network-concurrency=5", cap: 5, args: ["--network-concurrency", "5"], extraEnv: {} },
{ label: "BUN_CONFIG_MAX_HTTP_REQUESTS=5", cap: 5, args: [], extraEnv: { BUN_CONFIG_MAX_HTTP_REQUESTS: "5" } },
{
label: "--network-concurrency=2 overriding BUN_CONFIG_MAX_HTTP_REQUESTS=50",
cap: 2,
args: ["--network-concurrency", "2"],
extraEnv: { BUN_CONFIG_MAX_HTTP_REQUESTS: "50" },
},
])("bun install with $label doesnt go over $cap concurrent requests", async ({ cap, args, extraEnv }) => {
await withContext(defaultOpts, async ctx => {
const urls: string[] = [];
let maxConcurrentRequests = 0;
let concurrentRequestCounter = 0;
let totalRequests = 0;
setContextHandler(ctx, async function (request) {
concurrentRequestCounter++;
totalRequests++;
let concurrentRequests = 0;
setContextHandler(ctx, async function () {
concurrentRequests++;
maxConcurrentRequests = Math.max(maxConcurrentRequests, concurrentRequests);
try {
// Simulated registry latency: requests have to overlap for the stub to
// observe how many the client keeps in flight at once.
await Bun.sleep(10);
maxConcurrentRequests = Math.max(maxConcurrentRequests, concurrentRequestCounter);

if (concurrentRequestCounter > 20) {
throw new Error("Too many concurrent requests");
}
} finally {
concurrentRequestCounter--;
concurrentRequests--;
}

return new Response("404", { status: 404 });
});

const dependencies: Record<string, string> = {};
for (let i = 1; i <= 51; i++) {
dependencies[`bar${i}`] = "^1";
}
await writeFile(
join(ctx.package_dir, "package.json"),
`
{
"name": "foo",
"version": "0.0.1",
"dependencies": {
"bar1": "^1",
"bar2": "^1",
"bar3": "^1",
"bar4": "^1",
"bar5": "^1",
"bar6": "^1",
"bar7": "^1",
"bar8": "^1",
"bar9": "^1",
"bar10": "^1",
"bar11": "^1",
"bar12": "^1",
"bar13": "^1",
"bar14": "^1",
"bar15": "^1",
"bar16": "^1",
"bar17": "^1",
"bar18": "^1",
"bar19": "^1",
"bar20": "^1",
"bar21": "^1",
"bar22": "^1",
"bar23": "^1",
"bar24": "^1",
"bar25": "^1",
"bar26": "^1",
"bar27": "^1",
"bar28": "^1",
"bar29": "^1",
"bar30": "^1",
"bar31": "^1",
"bar32": "^1",
"bar33": "^1",
"bar34": "^1",
"bar35": "^1",
"bar36": "^1",
"bar37": "^1",
"bar38": "^1",
"bar39": "^1",
"bar40": "^1",
"bar41": "^1",
"bar42": "^1",
"bar43": "^1",
"bar44": "^1",
"bar45": "^1",
"bar46": "^1",
"bar47": "^1",
"bar48": "^1",
"bar49": "^1",
"bar50": "^1",
"bar51": "^1",
}
}`,
JSON.stringify({ name: "foo", version: "0.0.1", dependencies }),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--network-concurrency", "5"],

await using proc = spawn({
cmd: [bunExe(), "install", ...args],
cwd: ctx.package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
env: { ...env, ...extraEnv },
});
const err = await stderr.text();
expect(await exited).toBe(1);
expect(urls).toBeEmpty();
expect(maxConcurrentRequests).toBeLessThanOrEqual(5);
expect(totalRequests).toBe(51);
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(err).toContain("failed to resolve");
expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1."));
expect(maxConcurrentRequests).toBeLessThanOrEqual(cap);
expect({ totalRequests: ctx.requested, stdout, stderr, exitCode }).toEqual({
totalRequests: 51,
stdout: expect.stringContaining("bun install v1."),
stderr: expect.stringContaining("failed to resolve"),
exitCode: 1,
});
});
});

Expand Down
Loading