Files
OmniRoute/docs/proxy-port-clash-report.md
Diego Rodrigues de Sa e Souza 04683029a6 fix(build): exec native esbuild binary directly in prepublish (dast-smoke base-red) (#9558)
* fix(build): exec native tool binaries directly in runBuildTool

#8858 routed every resolved local bin through process.execPath to avoid
Windows .cmd shims — but esbuild >=0.25 ships bin/esbuild as the NATIVE
platform executable (ELF on Linux), so Node parsed machine code as JS and
build:cli died with 'SyntaxError: Invalid or unexpected token', turning
dast-smoke red for every PR.

runBuildTool now sniffs the entry's magic bytes (ELF / Mach-O / PE) and
execs native binaries directly; JS entries keep going through this Node
binary (the .cmd-shim avoidance #8858 wanted).

Validation (RED->GREEN on this box):
- RED: node node_modules/esbuild/bin/esbuild --version -> SyntaxError (ELF)
- GREEN: the exact failing CI step reproduced via the new logic bundles
  open-sse/mcp-server/server.ts successfully (4.2MB output, 1.3s).

* fix(docs): add MDX frontmatter to the 20 remaining docs without it

Same failure class as AGENTROUTER_WAF (#9503) and DOCKER_RELEASE_CHANNELS
(this run's dast-smoke red): any doc without frontmatter breaks the
fumadocs MDX loader during next build, killing build:cli/dast-smoke for
every PR. Swept ALL of docs/ (i18n mirrors excluded) in one pass so this
class cannot recur one file at a time.

* docs(env): document OMNIROUTE_INTERNAL_SERVICE_TOKEN(+_FILE), OPENROUTER_PROVIDER_STATS_* and embedded-Redis binding vars

Pre-existing env/docs contract drift from recently merged features made
check:env-doc-sync red for any docs-touching PR. Values and defaults read
from the defining modules (internalServiceAuth.ts, openrouterProviderStats.ts).

* fix(build): resolve bundled npm-cli.js in the standard Unix layout + safe npm fallback off-Windows

The opencode-plugin step hard-failed on GitHub runners because
resolveBundledNpmEntry only looked next to the node binary (Windows zip
layout); hostedtoolcache Node keeps npm at <prefix>/lib/node_modules/npm.
Added that candidate, and when neither exists on non-Windows the step now
falls back to plain 'npm' — the .cmd-shim hazard #8858 avoids is
Windows-only.

* test(mutation): register xai-agent-tools-passthrough.test.ts in stryker tap.testFiles

The test landed on release/v3.8.50 covering
open-sse/handlers/chatCore/passthroughHelpers.ts without the stryker
registration, so Fast Quality Gates' drift detection reds any PR that
carries it. Mechanical registration so its mutant kills count.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 02:19:57 -03:00

87 lines
3.6 KiB
Markdown

---
title: "Proxy Port Clash Investigation"
version: 3.8.50
lastUpdated: 2026-08-06
---
# Proxy Port Clash Investigation
## Summary
There is **no port clash** in the proxy auto-select / proxyFallback / proxyEgress system.
The proxy subsystem uses **pre-assigned registry ports** — it never binds to TCP ports
directly. The real EADDRINUSE history is in the **process supervisor** layer, where
the server's main listen port can clash during crash-loop restarts.
---
## Proxy Subsystem: No Port Binding
| Module | What It Does |
|---|---|
| `proxyAutoSelector.ts` | Selects a proxy config from the DB by applying health scores and rotation groups |
| `proxyFallback.ts` | Implements retry/fallback strategies when a selected proxy fails (try another proxy, then direct) |
| `proxyEgress.ts` | Probes/propagates egress IP info for logging — uses HTTP echo, not port binding |
| `proxyDispatcher.ts` | Creates `undici.ProxyAgent` dispatchers — these are HTTP-level (forward proxy), not TCP listen sockets |
| `proxyFetch.ts` | Patched global fetch that applies proxy dispatchers at the undici level |
None of these modules call `net.createServer()`, `http.createServer()`, or `app.listen()`.
Port management is entirely within the request life cycle — undici manages the TCP
connection pool internally.
**Fallback flow** (from `proxyFetch.ts` `runWithProxyContext`):
1. Try assigned proxy → proxy dispatcher
2. If unreachable → direct fallback (no dispatcher)
3. If still failing → error propagated up
No port allocation or release happens in this flow.
---
## Real EADDRINUSE Root Cause: Crash-Loop Restart Race
The actual port clash was in the **process supervisor** (`bin/cli/runtime/`):
| File | Role |
|---|---|
| `processSupervisor.mjs` | `ServerSupervisor` — spawns a child process, monitors exit code, restarts |
| `supervisorPolicy.mjs` | `waitUntilPortFree()`, `isPortFree()`, restart policy constants |
**Root cause:** When the server child process crashed and was immediately restarted, the
OS had not yet released the listen socket (TIME_WAIT / TCP lingering). The restart
attempt would bind to the same port and immediately fail with `EADDRINUSE`, causing
another crash → another restart → exhausted restart budget → gateway dead.
**Fix (#4425, in `supervisorPolicy.mjs`):**
1. Added `isPortFree(port)` — attempts a `net.createServer().listen()` on the target
port; resolves `false` if EADDRINUSE.
2. Added `waitUntilPortFree(port, timeoutMs=10000, intervalMs=250)` — polls every 250ms
for up to 10s until the port is free, then allows the restart.
3. Bumped `RESTART_RESET_MS` from 30s → 60s — the crash window was too short, causing
rapid cascading restarts inside the window.
4. Bumped `DEFAULT_MAX_RESTARTS` from 2 → 3 — more headroom for transient failures.
The `writePidFile()` / `killAllSubprocesses()` / `cleanupPidFile()` utilities in
`bin/cli/utils/pid.mjs` ensure clean PID file lifecycle.
## Related: Live-Dashboard EADDRINUSE (#6324)
A parallel fix (`live-ws-eaddrinuse-6324.test.ts`) ensures `startLiveDashboardServer()`
rejects with a proper `EADDRINUSE` error (instead of an unhandled socket 'error' event
that would crash the process). The dashboard server uses a separate port from the main
API server, so when both are configured on the same port, the second bind fails
gracefully.
---
## Current State
| Risk | Status | Remaining |
|---|---|---|
| Supervisor restart EADDRINUSE | **Fixed** (#4425) | None |
| LiveWS port clash | **Fixed** (#6324) | None |
| Proxy selection port clash | **Never applicable** | None |
| Two Redis CLIENT factories bind no TCP ports | **Never applicable** | None |
No further action needed on port clash.