Skip to content

feat: add HTTP/HTTPS proxy and TLS support (#40) - #43

Open
alerizzo wants to merge 3 commits into
mainfrom
feat/proxy-tls-support
Open

feat: add HTTP/HTTPS proxy and TLS support (#40)#43
alerizzo wants to merge 3 commits into
mainfrom
feat/proxy-tls-support

Conversation

@alerizzo

@alerizzo alerizzo commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #40. Supersedes #39.

Node's global fetch — used by the generated API client and the MITRE CVE lookup in commands/finding.ts — ignores HTTP_PROXY/HTTPS_PROXY/NO_PROXY, so the CLI is unusable behind a corporate proxy.

Approach

Delegate to configureProxy() from @codacy/tooling (0.1.00.22.0) — the same function the Codacy Analysis CLI calls. We write no proxy logic. Upstream installs a global undici dispatcher with per-request protocol + NO_PROXY routing, bare host:port normalization, and SSL_CERT_FILE/NODE_EXTRA_CA_CERTS CA loading.

Keeping the implementation upstream is the point: it's what makes the environment contract identical across the Codacy tools. A local copy would drift. AGENTS.md now records that proxy behavior changes belong in analysis-cli's packages/tooling/src/proxy.ts, followed by a dependency bump here.

src/utils/proxy.ts is a thin seam — configureProxyFromEnv() calls upstream and routes its throw into handleError(), giving red Error: <message> and exit 1 like every other failure here. It's a module rather than an inline try/catch because nothing in this repo has ever executed src/index.ts, so inline logic would be the only untestable branch in the feature.

Supported variables — same names as the Analysis CLI and the VS Code extension:

Variable Purpose
HTTPS_PROXY / HTTP_PROXY (or lowercase) Proxy URL per scheme; bare host:port accepted
NO_PROXY / no_proxy Hosts that bypass the proxy (*, .suffix), matched per request
SSL_CERT_FILE / NODE_EXTRA_CA_CERTS PEM CA bundle for a TLS-intercepting proxy
CODACY_CLI_INSECURE Disable TLS verification, last resort (warns on stderr)

Behaviorally a no-op when none are set — though not free; see the cost note below.

Why not #39

@rattalur found a real gap and their PR's docs — particularly the curl-works-but-the-CLI-doesn't diagnostic — were better than most. But that implementation hand-rolled the proxy logic with undici@8.10.1 as a direct dependency, and review found:

  • undici@8.10.1 requires Node >= 22.19.0, against this package's engines: ">=20". Verified on Node v20.20.2 (latest 20.x): require("undici") throws TypeError: webidl.util.markAsUncloneable is not a function at module load, so codacy --version crashed for every Node 20 user with no proxy configured. Tooling's undici@^6.21.0 supports Node >= 18.17.
  • NO_PROXY was evaluated once against the Codacy host, then a process-global agent was installed — so NO_PROXY=cveawg.mitre.org was ignored, and NO_PROXY=app.codacy.com disabled the proxy for everything.
  • A bare host:port crashed the CLI with a raw undici stack trace. That form is what curl accepts, i.e. exactly this feature's audience.
  • No CA support in codeSSL_CERT_FILE was silently ignored.

That Node 20 regression passed CI, because the proxy tests mocked undici wholesale and nothing executed the entry point. So this PR also adds a smoke step running the built CLI three ways (plain, with HTTPS_PROXY, and with a bad SSL_CERT_FILE expected to fail) on both matrix legs.

Verification

Beyond tsc --noEmit and 614 passing tests, verified against a local CONNECT-logging proxy:

Startup cost — measured, and now zero

@codacy/tooling@0.22.0 imported undici at module scope rather than behind configureProxy's "nothing configured" early-out, so every invocation paid for loading it — --help and --version included, proxy or not. Measured at 27 ms median against a main build.

0.23.0 fixes it, and this branch takes it. The import now sits behind a memoized lazy factory:

main   (no proxy code)  median 101 ms
this branch (0.23.0)    median 101 ms
delta                            0 ms     (was 27 ms on 0.22.0)

Verified the property directly, not just by timing: requiring the tooling barrel and calling configureProxy() with no proxy env leaves undici out of require.cache, while a configured proxy still loads it.

0.23.0 also replaces the bare Invalid URL a malformed proxy value used to produce with a message that names the setting and redacts credentials:

HTTPS_PROXY="ftp://user:hunter2@proxy.corp:8080"
→ Error: Invalid HTTPS_PROXY value "ftp://user:***@proxy.corp:8080":
  unsupported scheme "ftp:"; expected a URL such as http://proxy.corp:8080

Both changes were filed upstream from this branch's review and are recorded in analysis-cli's docs/tech-debt.md. Checked for upgrade breakage before taking it: the CA-bundle error text is unchanged, so the CI smoke step's substring assertion still holds, and bare host:port plus credentialed proxy URLs still resolve.

Note @codacy/tooling also stops being a phantom dependency: src/types/codacy-config.ts uses export type, which tsc erases, so dist/ previously contained no require("@codacy/tooling") at all.

Dependency is pinned exactly, after a correction

This branch first used ^0.22.0, on the reasoning that upstream proxy fixes would then arrive without a bump PR. That reasoning was wrong, and an adversarial review caught it: for a pre-1.0 package the caret spans patches only (^0.22.0>=0.22.0 <0.23.0-0), so it would never have picked up the 0.23.0 that actually carries the fixes. It bought silent patch-level drift — against a dependency this repo has no proxy coverage for, since the 4 unit tests mock it away — and none of the intended upside. Now 0.23.0, pinned exactly, bumps deliberate.

For the record, the related claim that "every other dependency here is pinned exactly" was also wrong: cli-table3 was already ^0.6.3 before this branch.

Known gap

The 4 unit tests mock @codacy/tooling entirely, so they pin only this repo's seam — that we delegate with no overrides, and that a throw becomes exit 1 with the message intact. NO_PROXY matching, scheme routing and bare-host handling are verified upstream and by the manual runs above, but nothing in this repo's CI would catch a regression in them. The smoke step covers construction plus the CA-failure message only. Called out rather than papered over; closing it properly means either integration tests with a local proxy here, or trusting upstream's suite, and that's a judgement call worth making explicitly.

🤖 Generated with Claude Code

Node's global fetch — used by the generated API client and the MITRE CVE
lookup in commands/finding.ts — ignores HTTP_PROXY/HTTPS_PROXY/NO_PROXY, so
the CLI was unusable behind a corporate proxy.

Rather than reimplement it, delegate to configureProxy() from @codacy/tooling
(bumped 0.1.0 -> ^0.22.0), the same function the Codacy Analysis CLI calls. It
installs a global undici dispatcher with per-request protocol and NO_PROXY
routing, bare host:port normalization, and SSL_CERT_FILE/NODE_EXTRA_CA_CERTS
CA loading. Keeping the implementation upstream is what keeps the environment
contract identical across the Codacy tools; a local copy would drift.

src/utils/proxy.ts is a thin seam: configureProxyFromEnv() calls it and routes
its deliberate fail-loud throw (unreadable or non-PEM CA bundle) into
handleError(), giving red `Error: <message>` and exit 1 like every other
failure here. analysis-cli exits 2 because it has a documented exit-code
scheme; this CLI does not, and exits 1 everywhere.

Called at the top of src/index.ts. Ordering is only constrained to precede
program.parse, since the dispatcher is resolved per request — it goes first so
the network stack is set up before we point it at the API. Kept top-level
rather than in the preAction hook so a typo'd SSL_CERT_FILE fails even on
--version.

Also add a CI smoke step that runs the built entry point three ways (plain,
with HTTPS_PROXY set, and with a bad SSL_CERT_FILE expected to fail). Nothing
previously executed src/index.ts — every command test builds a bare
new Command() — which is how a proxy dependency that cannot even load on
Node 20 could pass CI.

Upstream owns the proxy semantics and their 24 tests, so only the seam is
tested here (4 new tests, 614 total).

Supersedes #39.

Co-Authored-By: rattalur <145406381+rattalur@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Sep 7, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 17 complexity · 0 duplication

Metric Results
Complexity 17
Duplication 0

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

codacy-production[bot]

This comment was marked as resolved.

alerizzo and others added 2 commits September 8, 2026 11:15
Follow-up to an adversarial review of this branch. Three claims were wrong and
one dependency range did not do what it was chosen to do.

- Pin @codacy/tooling to exact 0.22.0, was ^0.22.0. The caret was chosen so
  upstream proxy fixes would arrive without a bump PR, but for a pre-1.0
  package the caret spans patches only (^0.22.0 resolves to >=0.22.0 <0.23.0-0),
  so it never would have picked up a 0.23.0. It bought silent patch drift with
  no proxy coverage in this repo to catch a regression, and none of the upside.
  Bumps are now deliberate. Also drops the claim that every other dependency
  here is pinned exactly -- cli-table3 was already ^0.6.3 before this branch.

- configureProxyFromEnv's doc claimed a bad CA bundle is "the one thing
  configureProxy throws on". It is not: a malformed proxy URL throws too, as
  whatever new URL() or undici's ProxyAgent raises. Verified against the
  installed 0.22.0 -- HTTPS_PROXY="not a url" gives `Error: Invalid URL`,
  ftp:// gives `Error: invalid url`. The catch is intentionally broad, so the
  comment now states that contract instead of enumerating a list that rots.

- "No-op when nothing is set" was true of behavior but not of cost. 0.22.0
  imports undici at module scope rather than behind configureProxy's early-out,
  so every invocation pays it, --help and --version included. Measured ~27 ms
  median against a ~119 ms baseline (20 interleaved runs, Node 20). Disclosed
  in the module header and the changeset rather than left implied.

Upstream 0.23.0 (published today) moves that import behind a lazy factory and
adds proxy-URL validation with credential redaction. Not taken here: it cannot
be installed or verified in this environment. Its CA-bundle error text is
unchanged, so the CI smoke step's substring assertion survives the bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0.23.0 ships the two fixes filed from this branch's review (recorded in
analysis-cli's docs/tech-debt.md), and both land here:

- undici now loads lazily, behind configureProxy's "nothing configured"
  early-out. An unproxied run pays nothing: `--version` measures 0 ms delta
  against a `main` build, down from 27 ms on 0.22.0 (20 interleaved runs,
  Node 20, median). Verified the property directly too -- requiring the
  tooling barrel and calling configureProxy() with no proxy env leaves undici
  out of require.cache, while a configured proxy still loads it.

- a malformed proxy URL now names the offending setting and redacts
  credentials, replacing a bare `Invalid URL`:

    HTTPS_PROXY="ftp://user:hunter2@proxy.corp:8080"
    -> Error: Invalid HTTPS_PROXY value "ftp://user:***@proxy.corp:8080":
       unsupported scheme "ftp:"; expected a URL such as http://proxy.corp:8080

Checked for upgrade breakage: the CA-bundle error text is unchanged, so the
CI smoke step's substring assertion still holds. Bare `host:port` and
credentialed proxy URLs still resolve.

Installing this required scoping npm's supply-chain guard rather than
weakening it. ~/.npmrc has min-release-age=3, which blocked a package
published an hour earlier; it now also carries
`min-release-age-exclude[]=@codacy/*` (npm's documented pattern for exactly
this). Verified the guard still blocks a 1-day-old third-party version.

Docs updated to match: the module header no longer claims a cost that is gone,
and the changeset no longer warns about it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codacy CLI is unable to connect to API when ran behing corporate proxy

1 participant