Fix: report the reason and exit cleanly when adk web / api_server fail to start - #904
Open
AmaadMartin wants to merge 4 commits into
Open
Fix: report the reason and exit cleanly when adk web / api_server fail to start#904AmaadMartin wants to merge 4 commits into
AmaadMartin wants to merge 4 commits into
Conversation
added 4 commits
August 10, 2026 19:35
adk web and adk api_server called process.exit(1) after logging the failure, which terminates before the winston Console transport drains a queued write, so the operator loses the reason on a pipe. Setting process.exitCode instead needs the listening socket released, because start() can reject after listen() has bound it. Also stringify a non-Error rejection, which previously rendered as undefined through an unchecked cast.
Six cases pin the log text, the stop() teardown, the swallowed stop() rejection, a non-Error rejection, a constructor failure and the untouched success path.
start() can leave AgentLoader watching the agents directory, and stop() closed only the HTTP handle, so 'adk web --a2a --reload_agents' stayed alive after a start-up failure printed its reason. stop() now disposes the loader, but only one the server built for itself: an injected loader belongs to the caller.
… each Both had a single caller. stop() always returns a promise, so the rejection needs .catch(), not a try/catch block.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Link to an existing issue (if applicable):
N/A
Or, if no issue exists, describe the change:
Problem:
adk webandadk api_serverlog a start-up failure and then callprocess.exit(1)in the same tick. The winston Console transport writes on a later tick, so the reason is dropped whenever output already sits queued in the pipe. The catch block also casts with(error as Error).message, so a non-Errorrejection printsundefined.Solution: The failure path now sets
process.exitCode = 1and lets the process end on its own, which lets the pending write drain. It also callsAdkApiServer.stop(), becausestart()can reject afterlisten()has bound the socket and the listening handle would otherwise keep the process alive forever.stop()now disposes the agent loader too, so--reload_agentscannot strand its file watcher; it disposes only a loader the server built for itself, because an injected loader belongs to the caller.Collision check:
gh pr list --repo AmaadMartin/adk-js --state open --limit 100, thengh pr diff --name-onlyon every open PR that touchesdev/src/cli/cli.ts(#900, #870, #839, #816, #803). None touches theweb/api_servercatch blocks. The nearest neighbour is #839, which setsprocess.exitCodein theintegration conformanceaction and adds an exit-code save/restore to the sharedbeforeEach; this PR keeps its save/restore local to its owndescribeblock, so the two only conflict textually. This PR branches frommain.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
npx vitest run --project unit:dev dev/test/cli/cli_test.ts— 32 passed.npx vitest run --project unit:dev dev/test/server/adk_api_server_test.ts— 56 passed.npx vitest run --project unit:dev dev/test/utils/agent_loader_test.ts— 36 passed.Six new cases in
dev/test/cli/cli_test.tscover both commands, the swallowedstop()rejection from a server that never bound, a plain-string rejection, a constructor failure with no server to stop, and the untouched success path. Three new cases indev/test/server/adk_api_server_test.tscover the loader disposal, the untouched injected loader, and astop()that never reachedlisten(). No existing test was edited.Coverage of the new lines is 100% statements and branches (read from the v8
coverage-final.json). Whole-filecli.tsmoves from 95.29% / 73.17% to 96.63% / 82.00%; the remainder is pre-existing.Proof the tests can fail. Each mutation was applied to
dev/src/cli/cli.tsalone and reverted after the run:process.exit(1)back in place ofprocess.exitCode = 1process.exit was called, andprocess.exit unexpectedly called with "1"await server?.stop()deletedexpected "spy" to be called 1 times, but got 0 timestry/catcharoundstop()removedpromise rejected "Error: Server is not running." instead of resolvingerror instanceof Errortest back to(error as Error).messageexpected "error" to be called with arguments: [ 'Error starting web server:', …(1) ]disposeAll()removed fromstop()expected "disposeAll" to be called 1 times, but got 0 timesownsAgentLoaderguard removedthis.agentLoader.disposeAll is not a functionOne
as unknown as Mockcast is added, on the module mock ofAdkApiServer. It is the pattern the rest of this file already uses, and it is needed because the test double is a partial instance. The three uses share one cast.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
A. The reason survives a pipe. With a 1 MB write already queued on stdout,
process.exit(1)loses the record andprocess.exitCodekeeps it:With an empty pipe buffer the small write reaches the reader either way, so this defect needs pending output to show itself.
B. A bind failure reports and exits.
C. No hang once the socket is bound. With
throw new Error("forced a2a failure")inserted at the top ofinitA2A(),adk web --a2aends in about two seconds:Dropping only the
stop()call from the same build reproduces the hang this PR avoids: the command printed the same message and was still running when a 30 second timeout killed it (exit=124,real 0m30.017s).D. No hang with
--reload_agents. The same forced failure, raised afterinitA2A()has listed the agents and so started the watcher, withadk web --a2a --reload_agents. Without the loader disposal the command outlived a 20 second timeout (exit=124,real 0m20.060s). With it:Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.
Review round 1
A complexity review asked for three changes, all applied.
stop()now releases the agent loader, which removes the--reload_agentshang the first revision documented as a follow-up. Thetry/catcharoundstop()became.catch(() => {}), anddescribeErrorwas inlined at its single call site.CI
run-tests(unit) passes, anddev/test/cli/cli_test.tsreports 32 passed on every runner. Therun-tests (ubuntu-latest)job fails on 7 pre-existingtests/integration/workflows/*sample tests. The same 7 failures appear on unrelated PR #903, so this branch does not cause them. The macOS and Windows jobs were cancelled by the matrix fail-fast, not by a failure of their own.