Skip to content

fix(hosting): close all site servers during shutdown - #11236

Open
jabrailkhalil wants to merge 6 commits into
firebase:mainfrom
jabrailkhalil:fix/hosting-stop-all-sites
Open

jabrailkhalil wants to merge 6 commits into
firebase:mainfrom
jabrailkhalil:fix/hosting-stop-all-sites

Conversation

@jabrailkhalil

@jabrailkhalil jabrailkhalil commented Oct 3, 2026 •

Copy link
Copy Markdown

Description

When serving multiple Hosting sites, each server overwrites the previous shutdown callback, so stopping Hosting closes only the last site's listener. The earlier sites remain running.

I now retain each server's destroyer and wait for all of them during shutdown. Cleanup keeps in-progress shutdowns visible to concurrent callers and preserves servers started while an earlier shutdown is finishing.

Scenarios Tested

Regression coverage includes two real HTTP servers that both serve a page before shutdown, repeated and overlapping stops, a new start during shutdown, and one server failing to close. Against the unchanged implementation, three cases fail, including the first real HTTP listener remaining open.

Current head: 6d590b5fc07e5a1eea6357c67374990f593ccc7e, merged with main afc3539aa7234f224b060820d832ef0834302990 on October 12. Only the changelog conflicted. I kept the new upstream Function Kit entry and the Hosting entry; the Hosting implementation, regression tests, package.json and npm-shrinkwrap.json are byte-identical to the previous head e975bf7. The PR still changes only the Hosting source, its tests and the changelog.

Fresh Windows validation (Node.js 22.21.1, npm 10.9.4; existing dependencies reused with the unchanged shrinkwrap):

  • Production build and test compilation pass on both the new head and the exact integrated main.
  • 171 selected tests pass, including all 12 Hosting cases, serve/utils lifecycle tests and the upstream projectConfig tests changed by main.
  • Full repository lint passes on a byte-exact LF Git archive. The first archive inherited Windows CRLF conversion and failed the line-ending check; I corrected the archive extraction and reran validation, without reformatting the tracked checkout or changing lint settings.
  • Running the actual all-tests glob through NYC/Mocha executes 5,676 passed / 19 pending / 100 failed on the new head versus 5,671 passed / 19 pending / 100 failed on clean integrated main in the same environment. All 100 failing test identities match exactly in order. The five additional passing cases are the Hosting regressions. This is a Windows head/main comparison, not a green Linux CI claim. Assertions, dependencies, platform gates and thresholds were not changed.
  • The repository script's single-quoted glob previously produced zero tests on Windows. I did not count that as a passing suite; the comparison above uses the real glob passed directly as an argument.

Historical Linux validation (Node.js 22.23.3, previous heads; not rerun on 6d590b5):

  • Initial fix (6186f28): full npm test passed lint and test compilation, with 5,667 tests passed and 11 pending. This includes all 12 Hosting tests.
  • After the error-handling review update (6b42eba): 109 lifecycle tests passed, including all 12 Hosting tests; test compilation and targeted ESLint passed. The strengthened test fails before the update and verifies that a pending sibling finishes before the original shutdown error is rethrown.
  • The production build run by npm ci passed.

Sample Commands

For a project with multiple Hosting configurations:

firebase serve --only hosting

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request updates the Firebase Hosting serve module to support stopping multiple sites concurrently by tracking server destroyers in a Set, and adds comprehensive unit tests to verify shutdown behaviors. The feedback identifies a critical issue in the stop function: if one server destroyer rejects, Promise.all will reject immediately, potentially leaving other servers running. A code suggestion is provided to ensure all destroyers are allowed to settle before throwing any encountered errors.

Comment thread src/serve/hosting.ts
Comment on lines +99 to 109
export async function stop(): Promise<void> {
await Promise.all(
Array.from(serverDestroyers, async (destroyServer) => {
try {
await destroyServer();
} finally {
serverDestroyers.delete(destroyServer);
}
}),
);
}

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.

high

If one of the server destroyers rejects, Promise.all will reject immediately, without waiting for the remaining servers to finish shutting down. This can leave those other servers running in the background, potentially causing port collision errors (such as EADDRINUSE) if the caller immediately attempts to restart the servers after stop() rejects.\n\nTo ensure all servers have fully completed their shutdown sequence before stop() returns or rejects, we should collect any errors and only throw after all destroyers have settled.

export async function stop(): Promise<void> {\n  const errors: unknown[] = [];\n  await Promise.all(\n    Array.from(serverDestroyers, async (destroyServer) => {\n      try {\n        await destroyServer();\n      } catch (err) {\n        errors.push(err);\n      } finally {\n        serverDestroyers.delete(destroyServer);\n      }\n    }),\n  );\n  if (errors.length > 0) {\n    throw errors[0];\n  }\n}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated in 6b42eba. I changed stop() to wait for every destroyer to settle before rethrowing the first error. The strengthened regression keeps the second shutdown pending, verifies that stop() remains pending, then checks completion and the original error identity. It fails before the fix and passes afterward. All 109 selected lifecycle tests pass, along with TypeScript compilation and targeted ESLint checks.

@jabrailkhalil

Copy link
Copy Markdown
Author

Merged current main in e975bf7 and resolved the changelog conflict without restoring already-released entries. The Hosting implementation and regression tests are unchanged from 6b42eba, including waiting for every sibling before rethrowing a shutdown error.

Fresh validation: frozen install/production build, test compilation and 100 lifecycle tests pass, including all 12 Hosting cases and the real two-listener regression. Lint passes on an exact LF Git snapshot. The actual full Windows test run gives 5,674 passed / 19 pending / 100 failed; clean current main gives 5,669 / 19 / 100 in the same environment, with the same failing test identities. I have kept the old green Linux run explicitly historical, not presented it as new-head CI, and changed no unrelated assertions, dependencies or lint rules.

The merge conflict is gone. Could a maintainer review this integration head and approve the upstream test workflows? I am not requesting a CI bypass.

@jabrailkhalil

jabrailkhalil commented Oct 20, 2026 •

Copy link
Copy Markdown
Author

I merged current main (4e6eb923) and resolved the changelog conflict, keeping the pending Hosting shutdown entry without bringing back entries that have already shipped. The Hosting implementation and tests are unchanged from the previous PR head.

Fresh Linux Node24 head/base comparison, checking out the exact source commits:

  • Updated PR head 6b0b364a: npm ci and full npm test -- -- --forbid-only pass; 5889 passing, 11 pending.
  • Current-main baseline: the same commands pass; 5884 passing, the same 11 pending.

That includes the normal publish build, lint/format checks, test compilation and complete unit suite. No checks or thresholds were relaxed. The PR still contains only the Hosting source, tests and changelog.

The fork's separate push-only Hosting integration jobs are not green without the upstream service-account/project secrets: Linux stops at authentication with an empty credential JSON, and Windows stops at the empty secret setup before tests. I am not claiming credential-backed cloud integration/E2E passed.

The conflict is cleared. Upstream CI is awaiting maintainer authorization; could a maintainer authorize that run and complete the review?

@jabrailkhalil

Copy link
Copy Markdown
Author

I merged current main and resolved the release changelog conflict, keeping the multi-site shutdown fix listed as unreleased. Validated 61a5a3f6 locally on Linux with Node v24.22.1: clean install/build, lint, TypeScript test compilation and the complete unit suite pass (5890 passing, 11 pending), including the real two-listener shutdown regression. Cloud Hosting deployment E2E was not run. Ready for review.

This branch has not been deployed

No deployments
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.

2 participants