Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

feat: proxy support (.npmrc + env-var cascade, HTTP/HTTPS/SOCKS)#476

Merged
zkochan merged 3 commits into
mainfrom
feat/proxy-support
May 13, 2026
Merged

feat: proxy support (.npmrc + env-var cascade, HTTP/HTTPS/SOCKS)#476
zkochan merged 3 commits into
mainfrom
feat/proxy-support

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Adds proxy support to pacquet, ported from pnpm v11 (SHA 94240bc046).

  • Config layer (feat(config)): Config.proxy: ProxyConfig + NoProxySetting enum. NpmrcAuth parses https-proxy, http-proxy, proxy (legacy), no-proxy, and noproxy from .npmrc. The env-var fallback cascade (HTTPS_PROXY/HTTP_PROXY/PROXY/NO_PROXY, each with upper- and lowercase lookups) matches upstream's config/reader/src/index.ts:591-600. .npmrc always wins over env; http_proxy falls back through the resolved https_proxy before consulting env.
  • Network layer (feat(network)): ThrottledClient::for_installs(&ProxyConfig) wires per-URL routing via reqwest::Proxy::custom. The closure dispatches HTTPS targets through https_proxy, HTTP through http_proxy, with a NoProxyMatcher (reverse-dot-segment-prefix port of upstream's checkNoProxy) short-circuiting both. Basic-auth userinfo is stripped from the URL and percent-decoded before being re-attached via Proxy::basic_auth, matching pnpm's decodeURIComponent(user):decodeURIComponent(pass). Invalid proxy URLs surface as ProxyError::InvalidProxy carrying diagnostic code ERR_PNPM_INVALID_PROXY.
  • CLI (feat(cli)): State::init switches to the config-aware constructor; InitStateError::Proxy propagates parse failures.

Reviewer flags

  • Enables reqwest's socks Cargo feature (in root Cargo.toml). It is a feature on an existing dep, not a new top-level dep, but it pulls a SOCKS implementation into the build. socks4/socks4a/socks5 proxy URLs are now honored.
  • crates/network/Cargo.toml gains direct deps on pacquet-config, derive_more, miette (all already in [workspace.dependencies]; no new top-level additions).
  • parse_proxy_url rejects any first-attempt parse that lacks a host, forcing the http://-prefix retry through the authority-bearing path. Rust's url::Url::parse("proxy.example:8080") is permissive enough to accept it as scheme + opaque path, which is not what users mean by a proxy shorthand.
  • Percent-decoding for the userinfo halves is a 15-line inline helper rather than a new percent-encoding direct dep — the only call site is the two halves of a proxy URL.

Test plan

  • cargo nextest run -p pacquet-config — 70 tests pass (11 new cascade tests + 1 EnvGuard smoke test)
  • cargo nextest run -p pacquet-network — 19 tests pass (matcher, parse, percent-decode, build, plus 2 mockito integration tests for proxy forwarding with basic-auth and noProxy bypass)
  • cargo nextest run --workspace — full 666-test suite pass
  • just ready — typos, fmt, check, lint clean
  • taplo format --check clean
  • RUSTDOCFLAGS=-D warnings cargo doc --no-deps --workspace --document-private-items clean
  • just dylint (perfectionist) clean
  • Manual smoke (out-of-band): with HTTPS_PROXY=http://localhost:9999 pacquet install, traffic routes to the proxy

Upstream coverage

Ports the unit-testable cases from pnpm's network/fetch/test/dispatcher.test.ts: per-URL routing, basic-auth decoding, noProxy reverse-dot-prefix match, bypass-all literal, invalid URL → ERR_PNPM_INVALID_PROXY, SOCKS-URL parse smoke. Skips upstream's describe.skip-ed live-SOCKS test.


Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • New Features

    • Added HTTP/HTTPS and SOCKS proxy support configurable via .npmrc and environment variables.
    • Added no-proxy bypass handling to exclude specified hosts from proxy routing.
    • Proxy settings now resolve via a cascade (npmrc → env) even when no .npmrc file exists.
  • Improvements

    • Improved proxy validation and clearer diagnostic errors for invalid proxy URLs.
    • Expanded tests and reliability around proxy parsing and routing behavior.

Review Change Stack

Copilot AI review requested due to automatic review settings May 13, 2026 18:29
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9de5b45c-32d3-491c-9c24-830252433969

📥 Commits

Reviewing files that changed from the base of the PR and between 5aed74b and b03ee45.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • crates/cli/src/state.rs
  • crates/config/src/lib.rs
  • crates/config/src/npmrc_auth.rs
  • crates/config/src/npmrc_auth/tests.rs
  • crates/network/Cargo.toml
  • crates/network/src/lib.rs
  • crates/network/src/proxy.rs
  • crates/network/src/tests.rs
  • crates/package-manager/src/install_package_from_registry/tests.rs
✅ Files skipped from review due to trivial changes (1)
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/package-manager/src/install_package_from_registry/tests.rs
  • crates/cli/src/state.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Dylint
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/network/src/proxy.rs
  • crates/config/src/lib.rs
  • crates/config/src/npmrc_auth.rs
  • crates/config/src/npmrc_auth/tests.rs
  • crates/network/src/tests.rs
  • crates/network/src/lib.rs
🧠 Learnings (3)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/network/src/proxy.rs
  • crates/config/src/lib.rs
  • crates/config/src/npmrc_auth.rs
  • crates/config/src/npmrc_auth/tests.rs
  • crates/network/src/tests.rs
  • crates/network/src/lib.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/config/src/npmrc_auth/tests.rs
  • crates/network/src/tests.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In this repo’s Rust tests (run via `cargo nextest`), stdout/stderr from passing tests is captured and is only shown when a test fails. As a result, `eprintln!` (and similar stderr logging) in these tests should generally not be flagged as CI “noise” on the happy path; CI output should only appear for failing tests.

Applied to files:

  • crates/network/src/tests.rs
🔇 Additional comments (7)
crates/network/src/proxy.rs (1)

1-217: LGTM!

crates/network/src/lib.rs (1)

1-267: LGTM!

crates/network/src/tests.rs (1)

1-279: LGTM!

crates/config/src/npmrc_auth/tests.rs (1)

1-474: LGTM!

crates/config/src/lib.rs (1)

1-1337: LGTM!

crates/config/src/npmrc_auth.rs (1)

1-367: LGTM!

crates/network/Cargo.toml (1)

13-18: Summary incorrectly claims pacquet-config is a new dependency.

The AI summary states "crates/network adds direct deps on pacquet-config, derive_more, and miette" but pacquet-config is not listed in the Cargo.toml (shown in the snippet). The types ProxyConfig, NoProxySetting, and ProxyError are defined locally in crates/network/src/proxy.rs and re-exported from the proxy module in lib.rs, not imported from pacquet-config.


📝 Walkthrough

Walkthrough

Adds proxy configuration types and parsing, applies an npmrc→legacy→env cascade to populate Config.proxy, builds a proxy-aware ThrottledClient (including SOCKS support), implements no-proxy matching and userinfo decoding, updates CLI wiring/error variants, and adds unit/integration tests.

Changes

Proxy Support Implementation

Layer / File(s) Summary
Proxy types and helpers
crates/network/src/proxy.rs
Adds ProxyConfig, NoProxySetting, ProxyError, parse_proxy_url, strip_userinfo, percent_decode_str, and NoProxyMatcher helpers for parsing and matching.
Config: .npmrc parsing and cascade
crates/config/src/npmrc_auth.rs, crates/config/src/lib.rs
NpmrcAuth now parses https-proxy, http-proxy, legacy proxy, and no-proxy/noproxy; adds apply_proxy_cascade and parse_no_proxy; Config exposes pub proxy: pacquet_network::ProxyConfig; Config::current applies the proxy cascade unconditionally; tests added to validate env fallback behavior.
Network crate: client construction, proxies, and tests
crates/network/src/lib.rs, crates/network/Cargo.toml, Cargo.toml, crates/network/src/tests.rs
Adds ThrottledClient::for_installs(&ProxyConfig) -> Result<_, ProxyError>, default_client_builder, and build_scheme_proxy; re-exports proxy types; enables reqwest socks feature in workspace Cargo.toml; adds deps/dev-deps and comprehensive unit/integration tests for proxy parsing, no-proxy matching, percent-decoding, and proxy-forwarding behavior.
CLI wiring and tests update
crates/cli/src/state.rs, crates/package-manager/src/install_package_from_registry/tests.rs
InitStateError variants renamed (Manifest, Lockfile) and new Proxy(ProxyError) variant added; State::init initializes HTTP client via ThrottledClient::for_installs(&config.proxy) with errors mapped to InitStateError::Proxy; tests updated to set proxy: Default::default() in test Config construction.

Sequence Diagram(s)

sequenceDiagram
  participant NpmrcAuth
  participant EnvLookup as Environment Lookup
  participant Config
  participant ProxyConfig
  participant ThrottledClient
  participant Parser as parse_proxy_url
  participant Matcher as NoProxyMatcher

  NpmrcAuth->>NpmrcAuth: parse .npmrc proxy keys
  NpmrcAuth->>EnvLookup: consult env fallbacks (HTTPS_PROXY/HTTP_PROXY/PROXY)
  NpmrcAuth->>ProxyConfig: apply_proxy_cascade -> resolved proxy values
  ProxyConfig-->>Config: set Config.proxy
  Config->>ThrottledClient: for_installs(&config.proxy)
  ThrottledClient->>Parser: parse_proxy_url(https/http proxy)
  Parser-->>ThrottledClient: parsed Url or ProxyError
  ThrottledClient->>Matcher: build NoProxyMatcher from no_proxy
  ThrottledClient->>ThrottledClient: default_client_builder + build_scheme_proxy (attach proxy closures)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • anthonyshew

🐰 A proxy path, now clearly drawn,
Through .npmrc and env at dawn,
No-proxy matches, userinfo decodes,
Request flows through carefully chosen roads,
SOCKS and HTTPS in harmony shown! 🌐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding proxy support with .npmrc and env-var cascade for HTTP/HTTPS/SOCKS protocols.
Description check ✅ Passed The PR description is comprehensive, including summary, upstream reference, test plan, and upstream coverage details. The description template requires a linked issue for non-Stage-1 or non-obvious approaches; the PR does reference upstream pnpm as the source and includes a linked SHA, which demonstrates alignment with the template's intent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/proxy-support

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/network/src/tests.rs (1)

29-66: ⚡ Quick win

Add failure-context logging around non-assert_eq! assertions.

These boolean assertions would be easier to diagnose under nextest if they log the relevant runtime context (host, matcher config, or error value) before asserting.

Based on learnings: In Rust test code, add logging for assertions like assert!/assert_ne! so failures are diagnosable without reruns.

Also applies to: 129-135, 154-160

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/network/src/tests.rs` around lines 29 - 66, The tests use bare assert!
calls that lack failure context; update each assertion in functions like
no_proxy_matcher_reverse_dot_match, no_proxy_matcher_empty_entries_never_match,
no_proxy_matcher_multiple_entries, no_proxy_bypass_short_circuits_every_host,
and no_proxy_none_matches_nothing to include diagnostic info by logging or
embedding a message with the runtime values (e.g., the tested host, the
NoProxyMatcher/NoProxySetting used, and any unexpected return from
NoProxyMatcher::matches_host) so failures show host and matcher config; you can
either call dbg!/eprintln! with the host and matcher before the assert or use
assert!(..., "host={:?} matcher={:?} expected=... got=...") to provide
actionable context.
crates/network/src/lib.rs (1)

246-254: 💤 Low value

Minor documentation inaccuracy: JS decodeURIComponent throws on invalid sequences.

The comment states this mirrors "decodeURIComponent's lenient fallback," but JavaScript's decodeURIComponent actually throws URIError on malformed sequences like %ZZ rather than keeping them verbatim. The current implementation (keeping invalid sequences) is actually safer and a reasonable choice—the comment could be clarified to say "unlike decodeURIComponent which throws, invalid sequences are kept verbatim."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/network/src/lib.rs` around lines 246 - 254, The doc comment for
percent_decode_str incorrectly claims it mirrors decodeURIComponent's "lossy
behavior" and lenient fallback; update the comment on percent_decode_str to
clarify that, unlike JavaScript's decodeURIComponent which throws a URIError on
malformed %XX sequences, this function intentionally keeps invalid
percent-escapes verbatim (and explain that this is a deliberate, safer
behavior), while preserving the rest of the descriptive text about why the
hand-rolled decoder exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/network/src/lib.rs`:
- Around line 246-254: The doc comment for percent_decode_str incorrectly claims
it mirrors decodeURIComponent's "lossy behavior" and lenient fallback; update
the comment on percent_decode_str to clarify that, unlike JavaScript's
decodeURIComponent which throws a URIError on malformed %XX sequences, this
function intentionally keeps invalid percent-escapes verbatim (and explain that
this is a deliberate, safer behavior), while preserving the rest of the
descriptive text about why the hand-rolled decoder exists.

In `@crates/network/src/tests.rs`:
- Around line 29-66: The tests use bare assert! calls that lack failure context;
update each assertion in functions like no_proxy_matcher_reverse_dot_match,
no_proxy_matcher_empty_entries_never_match, no_proxy_matcher_multiple_entries,
no_proxy_bypass_short_circuits_every_host, and no_proxy_none_matches_nothing to
include diagnostic info by logging or embedding a message with the runtime
values (e.g., the tested host, the NoProxyMatcher/NoProxySetting used, and any
unexpected return from NoProxyMatcher::matches_host) so failures show host and
matcher config; you can either call dbg!/eprintln! with the host and matcher
before the assert or use assert!(..., "host={:?} matcher={:?} expected=...
got=...") to provide actionable context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f71e987e-418d-40ae-90ae-415f411ad32f

📥 Commits

Reviewing files that changed from the base of the PR and between b799b7e and 5aed74b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • Cargo.toml
  • crates/cli/src/state.rs
  • crates/config/src/lib.rs
  • crates/config/src/npmrc_auth.rs
  • crates/config/src/npmrc_auth/tests.rs
  • crates/network/Cargo.toml
  • crates/network/src/lib.rs
  • crates/network/src/tests.rs
  • crates/package-manager/src/install_package_from_registry/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Dylint
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Cargo Deny
  • GitHub Check: Doc
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/package-manager/src/install_package_from_registry/tests.rs
  • crates/cli/src/state.rs
  • crates/network/src/tests.rs
  • crates/config/src/npmrc_auth.rs
  • crates/config/src/npmrc_auth/tests.rs
  • crates/config/src/lib.rs
  • crates/network/src/lib.rs
🧠 Learnings (3)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/package-manager/src/install_package_from_registry/tests.rs
  • crates/network/src/tests.rs
  • crates/config/src/npmrc_auth/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/package-manager/src/install_package_from_registry/tests.rs
  • crates/cli/src/state.rs
  • crates/network/src/tests.rs
  • crates/config/src/npmrc_auth.rs
  • crates/config/src/npmrc_auth/tests.rs
  • crates/config/src/lib.rs
  • crates/network/src/lib.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In this repo’s Rust tests (run via `cargo nextest`), stdout/stderr from passing tests is captured and is only shown when a test fails. As a result, `eprintln!` (and similar stderr logging) in these tests should generally not be flagged as CI “noise” on the happy path; CI output should only appear for failing tests.

Applied to files:

  • crates/network/src/tests.rs
🔇 Additional comments (17)
Cargo.toml (1)

64-70: LGTM!

crates/network/Cargo.toml (1)

14-23: LGTM!

crates/package-manager/src/install_package_from_registry/tests.rs (1)

56-56: LGTM!

crates/config/src/npmrc_auth/tests.rs (1)

6-223: LGTM!

crates/cli/src/state.rs (1)

5-5: LGTM!

Also applies to: 34-40, 62-67

crates/config/src/lib.rs (1)

102-145: LGTM!

Also applies to: 309-315, 639-696, 1100-1143

crates/network/src/tests.rs (1)

1-27: LGTM!

Also applies to: 68-127, 137-153, 162-258

crates/config/src/npmrc_auth.rs (1)

1-1: LGTM!

Also applies to: 5-10, 19-31, 58-65, 72-117, 120-131

crates/network/src/lib.rs (9)

1-9: LGTM!


11-12: LGTM!


139-174: LGTM!


187-202: LGTM!


204-223: LGTM!


225-244: LGTM!


273-297: LGTM!


299-347: LGTM!


349-369: LGTM!

@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00     15.8±0.06ms   274.3 KB/sec    1.00     15.8±0.59ms   274.0 KB/sec

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end proxy support to pacquet (ported from pnpm v11), spanning config parsing/cascade (.npmrc + env), network client proxy routing (HTTP/HTTPS/SOCKS), and CLI state initialization with proxy-aware client construction.

Changes:

  • Introduces ProxyConfig + NoProxySetting and extends .npmrc parsing to read proxy keys and apply an upstream-matching env-var fallback cascade.
  • Wires proxy routing into pacquet-network via reqwest::Proxy::custom, including noProxy matching and proxy basic-auth handling, plus integration/unit tests.
  • Updates CLI initialization to build the install HTTP client using the resolved proxy config; enables reqwest’s socks feature.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/package-manager/src/install_package_from_registry/tests.rs Updates test config construction to include the new Config.proxy field.
crates/network/src/tests.rs Adds unit + mockito integration tests covering proxy routing, auth decoding, no-proxy behavior, and invalid proxy URL diagnostics.
crates/network/src/lib.rs Implements proxy-aware ThrottledClient::for_installs, noProxy matcher, proxy URL parsing, and ProxyError diagnostic.
crates/network/Cargo.toml Adds direct deps needed for proxy config/diagnostics and dev-deps for mockito + tokio test runtime.
crates/config/src/npmrc_auth/tests.rs Adds focused tests for .npmrc proxy key parsing and env cascade behavior without mutating process env.
crates/config/src/npmrc_auth.rs Extends .npmrc parser and applies proxy/env cascade into Config.proxy; adds no-proxy parsing.
crates/config/src/lib.rs Introduces ProxyConfig/NoProxySetting, threads proxy config through Config, and ensures cascade runs even without an .npmrc.
crates/cli/src/state.rs Builds install HTTP client using ThrottledClient::for_installs(&config.proxy) and plumbs ProxyError into init errors.
Cargo.toml Enables reqwest socks feature.
Cargo.lock Locks new/updated dependency graph due to added features/deps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/config/src/lib.rs Outdated
Comment thread crates/network/src/tests.rs Outdated
@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.47917% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.25%. Comparing base (d8e7807) to head (b03ee45).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
crates/network/src/proxy.rs 98.48% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #476      +/-   ##
==========================================
+ Coverage   89.61%   90.25%   +0.64%     
==========================================
  Files         119      120       +1     
  Lines       11098    11274     +176     
==========================================
+ Hits         9945    10175     +230     
+ Misses       1153     1099      -54     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.509 ± 0.092 2.372 2.640 1.00
pacquet@main 2.511 ± 0.110 2.412 2.751 1.00 ± 0.06
pnpm 5.668 ± 0.055 5.577 5.729 2.26 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.50910181082,
      "stddev": 0.0924958671135193,
      "median": 2.49958406352,
      "user": 2.59299372,
      "system": 3.4954548999999995,
      "min": 2.37169913052,
      "max": 2.64039704552,
      "times": [
        2.50438981352,
        2.62782867652,
        2.59065002052,
        2.49477831352,
        2.39618989552,
        2.48271599652,
        2.64039704552,
        2.43723775952,
        2.54513145652,
        2.37169913052
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.51064699442,
      "stddev": 0.10999635268098712,
      "median": 2.4650736375199997,
      "user": 2.6100618199999994,
      "system": 3.4520275000000007,
      "min": 2.41212761552,
      "max": 2.75083732852,
      "times": [
        2.57885647552,
        2.43013646052,
        2.58285924752,
        2.41212761552,
        2.57820261452,
        2.44893219652,
        2.75083732852,
        2.42744370552,
        2.41585922152,
        2.48121507852
      ]
    },
    {
      "command": "pnpm",
      "mean": 5.668226666920001,
      "stddev": 0.05458097522551744,
      "median": 5.68664131202,
      "user": 8.206329019999998,
      "system": 4.2088825,
      "min": 5.57737465652,
      "max": 5.72904537652,
      "times": [
        5.72904537652,
        5.61614964352,
        5.61089960452,
        5.71387998552,
        5.71311175952,
        5.66328176452,
        5.57737465652,
        5.71000085952,
        5.71246532552,
        5.63605769352
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 723.7 ± 30.2 702.9 807.6 1.00
pacquet@main 847.4 ± 88.9 754.2 974.6 1.17 ± 0.13
pnpm 2406.5 ± 138.4 2291.1 2660.7 3.33 ± 0.24
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.72368352764,
      "stddev": 0.030244646253980476,
      "median": 0.71613424394,
      "user": 0.36098489999999994,
      "system": 1.61454634,
      "min": 0.70292672994,
      "max": 0.8076313649400001,
      "times": [
        0.8076313649400001,
        0.71688721994,
        0.71658193494,
        0.71568655294,
        0.7119927999400001,
        0.72339702094,
        0.71330568094,
        0.7049893109400001,
        0.70292672994,
        0.72343666094
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.8473845346400001,
      "stddev": 0.08885313699775434,
      "median": 0.80708801844,
      "user": 0.3588743,
      "system": 1.6289679399999997,
      "min": 0.7542094019400001,
      "max": 0.97461032794,
      "times": [
        0.96124400194,
        0.8040948439400001,
        0.97461032794,
        0.8549959219400001,
        0.77048862194,
        0.78863505594,
        0.97441436394,
        0.78107161394,
        0.7542094019400001,
        0.81008119294
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.40650303814,
      "stddev": 0.13842249689950184,
      "median": 2.34283956644,
      "user": 2.7767676999999997,
      "system": 2.14342684,
      "min": 2.29105102294,
      "max": 2.66074321894,
      "times": [
        2.55545016294,
        2.31183661694,
        2.66074321894,
        2.35033023394,
        2.33534889894,
        2.29105102294,
        2.36905562194,
        2.29251762794,
        2.31324534594,
        2.58545163094
      ]
    }
  ]
}

zkochan added 3 commits May 13, 2026 20:54
Ports pnpm v11's `getDispatcher` (`network/fetch/src/dispatcher.ts` at
SHA 94240bc046) onto reqwest, with the resolved proxy configuration
modeled by the new `ProxyConfig` + `NoProxySetting` types in
`pacquet-network`. HTTPS targets route through `https_proxy`; HTTP
targets through `http_proxy`; the `no_proxy` field short-circuits both
via a reverse-dot-segment-prefix matcher that mirrors upstream's
`checkNoProxy` semantics (`npmjs.org` matches `registry.npmjs.org` but
not `evilnpmjs.org`).

The proxy types live in `pacquet-network` rather than `pacquet-config`
because `pacquet-config` already depends on `pacquet-network` for the
auth-headers plumbing (#337), so the reverse direction would form a
cycle. The downstream `pacquet-config` will hold a
`Config.proxy: ProxyConfig` and populate it from `.npmrc` + env in the
follow-up commit.

Basic-auth userinfo embedded in the proxy URL is stripped and
percent-decoded before being forwarded via `Proxy::basic_auth`, matching
pnpm's `decodeURIComponent(user):decodeURIComponent(pass)` step. The
percent-decoder is a 15-line inline helper rather than a new direct
`percent-encoding` dep, since it only runs against the two halves of a
proxy userinfo. Unlike JavaScript's `decodeURIComponent`, which throws
on malformed sequences, the helper keeps invalid `%XX` escapes verbatim
— the safer behavior in a config path where the alternative would be
rejecting a half-broken password.

`parse_proxy_url` retries failed parses with an `http://` prefix to
support shorthand values like `proxy.example:8080`, matching pnpm's
`parseProxyUrl`. Rust's `url` parser is permissive enough to accept the
shorthand as scheme + opaque path, so the parser also rejects any first-
attempt parse that lacks a host — forcing the retry through that
authority-aware path.

Invalid proxy URLs surface as `ProxyError::InvalidProxy` with diagnostic
code `ERR_PNPM_INVALID_PROXY`, matching upstream's error code. Failure
is detected eagerly at client-build time (same as pnpm's
`getProxyAgent`) rather than lazily per-request.

Enables reqwest's `socks` feature so socks4/socks4a/socks5 URLs are
honored — pnpm supports the same set via its socks-client wrapper.

The existing `new_for_installs()` is preserved as a thin wrapper around
`for_installs(&ProxyConfig::default())` so test fixtures and the
benchmark harness keep their call sites unchanged.

Tests port the unit-testable describe blocks from
`network/fetch/test/dispatcher.test.ts`: per-URL routing, basic-auth
decoding, noProxy reverse-dot-prefix match, bypass-all literal, invalid
URL diagnostic code, and SOCKS-URL parse smoke. Two mockito integration
tests cover end-to-end HTTP proxy forwarding (with decoded
`Proxy-Authorization`) and the noProxy-bypass path.
Adds `Config.proxy: ProxyConfig` (the type lives in `pacquet-network`,
see preceding commit) and extends `NpmrcAuth` to capture the four
proxy keys (`https-proxy`, `http-proxy`, `proxy`, `no-proxy`, `noproxy`)
plus the env-var fallback cascade pnpm 11 runs in
`config/reader/src/index.ts:591-600` (SHA 94240bc046). The cascade now
runs unconditionally from `Config::current` — even when no `.npmrc` is
present — so the env fallback fires the same way it does in pnpm.

The new `NpmrcAuth::apply_proxy_cascade::<Api: EnvVar>` is generic over
the project-wide `EnvVar` capability trait (introduced by #339), so
cascade unit tests inject the env without taking `EnvGuard`'s global
lock. The existing `apply_to` test helper now runs the full three-phase
sequence (`apply_registry_and_warn` → `apply_proxy_cascade` →
`build_auth_headers`).

`no-proxy=true` (literal) is upstream's `noProxy: string | true`
"bypass every proxy" shape and is parsed as `NoProxySetting::Bypass`.
Comma-separated host lists become `NoProxySetting::List`, trimmed with
empties dropped — the network layer reverse-dot-prefix-matches against
`List` entries when applying the cascade.

The cascade is invoked from `Config::current` between
`apply_registry_and_warn` (phase 1) and `build_auth_headers` (phase 2)
of the existing auth flow. Phase placement is incidental — the proxy
cascade is independent of the registry and creds layers — but slotting
it there keeps every `.npmrc`-consuming step in one block of the
function for the reader.

Tests cover the parse arms (each proxy key, the `no-proxy`/`noproxy`
last-wins alias), the cascade branches (legacy `proxy` → https slot,
http inheriting resolved https, env fallback only when .npmrc unset,
.npmrc winning over env, `PROXY` env fallback, lowercase-only env), and
the `noProxy: true` → `Bypass` parse. A `static_env!` macro keeps each
test's env-table inline. A real-`std::env::var` smoke test in `lib.rs`
exercises the path through `Config::current` under `EnvGuard`.

The `Config` literal in
`crates/package-manager/src/install_package_from_registry/tests.rs`
gains the `proxy` field (defaults to `ProxyConfig::default()`).
Switches `crates/cli/src/state.rs` from `ThrottledClient::new_for_installs()`
to `ThrottledClient::for_installs(&config.proxy)` so the install client
honors the `.npmrc` / env proxy cascade landed in the preceding two
commits. Proxy build failures surface as a new `InitStateError::Proxy`
variant carrying `ProxyError` (transparently diagnosed as
`ERR_PNPM_INVALID_PROXY`).

Drops the `Load` prefix on `InitStateError`'s variants
(`LoadManifest` → `Manifest`, `LoadLockfile` → `Lockfile`) so clippy's
`enum_variant_names` lint stops firing once a third `Load*`-prefixed
variant pushes the type past its shared-prefix threshold. The variants
are still self-descriptive inside `InitStateError::*`; no public
consumers exist outside `state.rs`.

Folds in `cargo fmt` reflows of three test files
(`crates/config/src/npmrc_auth/tests.rs`, `crates/network/src/lib.rs`,
`crates/network/src/tests.rs`) — trivial line-joins on lines that just
fit under the 100-column budget.
@zkochan zkochan force-pushed the feat/proxy-support branch from 5aed74b to b03ee45 Compare May 13, 2026 19:12
@zkochan

zkochan commented May 13, 2026

Copy link
Copy Markdown
Member Author

Rebase + review-comment pass

Force-pushed onto the latest main (which now includes the auth feature #337 and the EnvVar capability trait from #339). The proxy stack was rewritten on top of the new auth plumbing — see the three commits in this branch — and the inline review comments are addressed:

CodeRabbit nitpicks

  • crates/network/src/tests.rs: failure-context logging around non-assert_eq! assertions — added in 5d2865d. Each assert! now embeds the relevant runtime context in the message (host, matcher, error value) per the repo's test-logging convention. Boolean assertions also have an eprintln! immediately above for the matcher/error state when applicable.
  • crates/network/src/lib.rs: decodeURIComponent lossy-fallback inaccuracy — fixed in 5d2865d. The doc for percent_decode_str now says explicitly: "Unlike JavaScript's decodeURIComponent, which throws URIError on malformed %XX sequences (e.g. %ZZ), this function intentionally keeps invalid sequences verbatim. The lenient fallback ... is the safer choice in a config path where the alternative is rejecting a half-broken password value."

Copilot inline comments

  • crates/config/src/lib.rs:105 doc visibility mismatch — resolved by the rewrite. The cascade now lives in NpmrcAuth::apply_proxy_cascade::<Api: EnvVar> (using the project-wide EnvVar capability trait) and the stale "private" wording is no longer in the codebase.
  • crates/network/src/tests.rs:209 userate typo — fixed in 5d2865d. The explanatory comment now reads user@name:p@ss percent-encoded → user%40name:p%40ss; ... base64-encodes the pair as dXNlckBuYW1lOnBAc3M=.

Architectural change from rebase

The auth feature on main made pacquet-config depend on pacquet-network (for AuthHeaders). To break what would have been a circular dep, ProxyConfig and NoProxySetting now live in pacquet-network (next to AuthHeaders) and pacquet-config holds Config.proxy: pacquet_network::ProxyConfig. The cascade implementation (NpmrcAuth::apply_proxy_cascade::<Api: EnvVar>) is generic over the EnvVar capability trait introduced in #339, so cascade unit tests inject the env via a per-test unit struct rather than mutating the real process environment.

CI parity sweep on the rebased branch: just ready, taplo format --check, RUSTDOCFLAGS=-D warnings cargo doc --no-deps --workspace --document-private-items, and just dylint all clean. 1048 workspace tests pass.


Written by an agent (Claude Code, claude-opus-4-7).

@zkochan zkochan merged commit 610ffee into main May 13, 2026
16 checks passed
@zkochan zkochan deleted the feat/proxy-support branch May 13, 2026 19:30
zkochan added a commit that referenced this pull request May 13, 2026
Closes #482.

## Summary

Ports pnpm v11's TLS + `local-address` `.npmrc` keys onto pacquet
(SHA [`94240bc046`](https://github.com/pnpm/pnpm/blob/94240bc046/network/fetch/src/dispatcher.ts)),
the natural pair to the proxy support that landed in #476. Three layers:

- **`feat(network)`** (e9ed56c): adds `TlsConfig` next to `ProxyConfig` in `pacquet-network`, with `ca: Vec<String>`, `cert`/`key`, `strict_ssl: Option<bool>`, and `local_address: Option<IpAddr>`. `ThrottledClient::for_installs` gains a `&TlsConfig` parameter; the unified error surface is the new `ForInstallsError` enum carrying either `ProxyError` or `TlsError`. CAs route through `Certificate::from_pem`, client identities through `Identity::from_pkcs8_pem` (the only PKCS path reqwest exposes on the native-tls backend pacquet builds with). `strict_ssl` defaults to `true` at build site, matching pnpm's per-emit-site `strictSsl ?? true` rather than a config-layer default.
- **`feat(config)`** (e8dcd87): extends `NpmrcAuth` with the six new keys (`ca`, `cafile`, `cert`, `key`, `strict-ssl`, `local-address`) and adds `apply_tls_and_local_address` to populate `Config.tls`. `cafile` reads from disk and splits on `-----END CERTIFICATE-----` to mirror pnpm's [`loadCAFile`](https://github.com/pnpm/pnpm/blob/94240bc046/config/reader/src/loadNpmrcFiles.ts#L249-L265). Unreadable `cafile` is silently treated as unset. Invalid `strict-ssl` and `local-address` values drop silently.
- **`feat(cli)`** (1759f9d): one-line swap in `State::init` to pass `&config.tls` instead of `TlsConfig::default()`. Folds in two CI-parity fixups: the self-signed test cert moves to a shared `.pem` fixture under `crates/network/tests/fixtures/` (typos linter false positives on base64 DER), and four reqwest intra-doc links become plain backticks.

## Parity policy

Faithful to pnpm — see the [research brief](#482) on the issue. Highlights:

- **No new error codes.** pnpm doesn't define `ERR_PNPM_INVALID_CA` etc.; invalid PEMs surface as raw `tls.connect` errors at request time upstream. Pacquet validates eagerly via `Certificate::from_pem` / `Identity::from_pkcs8_pem` (pushing the failure to per-request time would silently degrade every install behind a broken `ca`) but deliberately omits a `code(...)` attribute on `TlsError` so reviewers can see at a glance it's a pacquet-only diagnostic, not a pnpm error code.
- **Silent `cafile`-not-found.** Matches pnpm's `catch {}` swallow in `loadCAFile`.
- **No env-var fallback.** pnpm reads only `.npmrc`; Node's implicit `NODE_EXTRA_CA_CERTS` / `NODE_TLS_REJECT_UNAUTHORIZED` honoring doesn't apply to pacquet's reqwest stack.
- **`strict-ssl: false` disables both chain-of-trust and hostname verification**, matching Node's `rejectUnauthorized=false` short-circuit (pacquet uses reqwest's `danger_accept_invalid_certs(true)` which has the same combined semantics).

## Reviewer flags

- **PKCS#8-only client keys.** Reqwest's native-tls backend exposes only `Identity::from_pkcs8_pem`; legacy PKCS#1 keys (`-----BEGIN RSA PRIVATE KEY-----`) and PKCS#12 bundles are not supported by this constructor. Documented at the `apply_tls` callsite with the `openssl pkcs8 -topk8 -nocrypt` conversion command. Switching to rustls-tls would broaden the supported formats but is out of scope here.
- **Per-registry TLS overrides** (`//host:cafile=`, `//host:ca=`, `//host:cert=`, `//host:key=`) are **not** included. Same shape as the existing scoped-auth handling but a sizeable feature on its own; flagged in #482 as a follow-up.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants