Fix proxy exiting with code 0 when server.ListenAndServe() fails (e.g. bind address already in use) - #219
Conversation
main() logged fatal errors (config parse failure, sentry setup failure, ListenAndServe bind failure, proxy.Close failure) via log.Println(err) followed by a bare return, which lets the process exit normally with code 0. Integrations that gate on exit code (e.g. github/codeql-action) treated this as success even when the proxy never started, e.g. on "address already in use". Replace each of those branches with log.Fatal(err), which logs then calls os.Exit(1). This matches the existing convention already used elsewhere in this codebase for fatal setup errors (proxy.go's setCA and cache.New failures, logging.go's log file open failure). Add main_test.go covering the changed exit-code paths using the standard Go "helper process" pattern (TestMain re-execs the test binary via os.Args[0] under an env var to run the real main()): - bind-address-in-use now exits non-zero (the reported defect) - graceful SIGTERM shutdown still exits 0 (regression guard) - invalid config path exits non-zero Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Ensures fatal proxy startup/runtime errors return a non-zero exit code.
Changes:
- Replaces silent error returns with
log.Fatal. - Adds subprocess tests for bind failures, invalid configuration, and graceful shutdown.
Show a summary per file
| File | Description |
|---|---|
main.go |
Exits non-zero on fatal errors. |
main_test.go |
Adds exit-code regression tests. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Suppressed comments (1)
main_test.go:132
os.Process.Signal(syscall.SIGTERM)is unsupported on Windows, where it returnsos.ErrProcessDone/EWINDOWSrather than delivering a graceful signal. This makesgo testfail for a platform this repository explicitly builds; skip this signal-based assertion on Windows (cleaning up the child), or use a Windows-specific console control event.
require.NoError(t, cmd.Process.Signal(syscall.SIGTERM))
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
Revert the log.Fatal change for config.Parse, setupSentry, and proxy.Close - we do not know whether their silent exit-0 behavior on error was intentional, so leave them untouched. Only the server.ListenAndServe() bind-failure branch (the reported defect) now exits non-zero via log.Fatal. Also remove TestInvalidConfigPath_ExitsNonZero, which exercised the now-reverted config.Parse branch. The ListenAndServe-focused tests (address-in-use, graceful shutdown) remain. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Assert the platform-independent "listen tcp <addr>: bind:" prefix (generated by Go's net package) instead of the OS-specific error text, which differs on Windows (e.g. "Only one usage of each socket address..."). - Skip the SIGTERM-based graceful shutdown assertion on Windows, where os.Process.Signal(syscall.SIGTERM) cannot deliver a graceful shutdown signal (it returns os.ErrProcessDone/EWINDOWS instead). The helper process is killed and reaped before skipping so it doesn't leak. - Verified main.go and main_test.go still build and the test binary still compiles when cross-compiled for GOOS=windows. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
No-op commit to re-trigger workflow runs after a transient GitHub API rate-limit failure in the unrelated Smoke/e2e artifact-download step. No functional changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
I pushed a follow-up because the current The proxy originally used I traced the current bug back to that cleanup change. Returning from Go's I considered replacing I moved the proxy work into a separate This keeps the cleanup from PR 37 and restores the failure signal that CodeQL needs. I also restored the invalid-config regression test and updated the new process-test helpers to satisfy the repository's context-aware lint rules. |
There was a problem hiding this comment.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
main_test.go:124
- After
Startsucceeds, any laterrequirefailure (especially the readiness timeout or signal failure) exits the test without callingWait, leaving the helper process running until context cancellation and unreaped afterward. Register cleanup immediately so every failure path kills and waits for the child; the normal path remains unaffected becauseProcessStateis set afterWait.
require.NoError(t, cmd.Start())
- Files reviewed: 2/2 changed files
- Comments generated: 3
- Review effort level: Balanced
Problem
main()inmain.gohandled theserver.ListenAndServe()failure case with the pattern:A bare
returnfrommain()allows the process to exit normally, which Go reports as exit code 0, even though a fatal error occurred (a real listen/bind failure, as opposed tohttp.ErrServerClosed, which is the expected result of a graceful shutdown).Why this matters
This can happen for two distinct reasons:
In both cases, the proxy logged the bind error and then exited with status
0. Any integration that gates on exit code interpreted that as a successful start and proceeded as if the proxy were listening and ready to serve traffic, when in fact no proxy was running. This is a silent failure: the calling process has no reliable signal that something went wrong.Concrete impact:
github/codeql-actionThis is not a hypothetical concern —
github/codeql-action'sstart-proxyaction explicitly relies onupdate-job-proxy's exit code to detect a failed startup and retry on a different port. Seestart-proxy-action.ts#L164-L170:This handler is the only signal the action has that the proxy failed to bind its port; when
code === 0it assumes the previous attempt succeeded and moves on to report a successful start (logger.info(\Proxy started on ${host}:${port}`)) using a port that, in reality, has no proxy listening on it. With the current bug, a bind failure always reportscode === 0`, so this retry-on-a-different-port logic never triggers, and CodeQL/Actions traffic that should have gone through the MITM proxy for private registry authentication is silently misrouted or fails downstream instead of being caught and retried at the source.Reproduction of the current (buggy) behavior
The second proxy process fails to bind the already-occupied port, logs the error, and still exits with code
0, matching the second scenario above (a second runner's proxy process colliding with a first one already listening on the same host).Fix
Replace the
log.Println(err); returnpattern in theserver.ListenAndServe()branch withlog.Fatal(err), which logs the error and then callsos.Exit(1). This is a one-line change, scoped strictly to this branch. The existingerr != http.ErrServerClosedguard is preserved, so a graceful shutdown (SIGINT/SIGTERM) still exits0as before — only a genuine listen/bind failure now propagates a non-zero exit code, allowing consumers like thestart-proxyaction's retry logic above to work as designed.This matches the convention already established elsewhere in this codebase for fatal setup errors:
proxy.go'ssetCAandcache.Newfailures, andlogging.go's log-file-open failure, all already uselog.Fatal(err).Scope note: the
config.Parse,setupSentry, andproxy.Closeerror branches inmain()use the samelog.Println(err); returnpattern and technically share the same silent-exit-0 characteristic. This PR intentionally leaves them untouched, since it's unclear whether that behavior was a deliberate design choice for those specific cases, and the reported defect is specifically aboutListenAndServe. Changing them can be considered separately if desired.Test coverage
Since
log.Fatalterminates the process, the resulting exit code can't be observed in-process.main_test.gouses the standard Go "helper process" pattern (the same technique used in the Go standard library's ownos/execandos/signaltests):TestMainre-executes the already-compiled test binary viaos.Args[0]under an environment variable flag, so the subprocess runs the realmain()and its exit code reflects actual behavior.New tests:
TestListenAndServe_AddressInUse_ExitsNonZero— pre-binds a port, then launches the proxy against that same address and asserts the process now exits non-zero. This directly reproduces and guards against the reported defect (both the coincidental-collision and concurrent-runner-processes scenarios manifest identically at this level: two processes contending for one port).TestGracefulShutdown_ExitsZero— starts the proxy, sendsSIGTERMonce it's listening, and asserts it still exits0. Regression guard for the normal shutdown path.Verification
go build ./...go vet ./...gofmt -l .(clean)go test -race -shuffle=on -count=2 ./...— all packages passCo-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com