Skip to content

fix: gracefully handle sends on closed server during shutdown#70

Closed
shields wants to merge 5 commits into
homebridge:latestfrom
shields:fix/shutdown-race-condition
Closed

fix: gracefully handle sends on closed server during shutdown#70
shields wants to merge 5 commits into
homebridge:latestfrom
shields:fix/shutdown-race-condition

Conversation

@shields
Copy link
Copy Markdown
Contributor

@shields shields commented May 19, 2026

(This is a duplicate of PR #60, which was closed accidentally.)

MDNSServer.sendResponse, sendOnAllNetworksForService, and send now gracefully handle the closed state instead of throwing ServerClosedError. Responder.shutdown cancels pending delayed multicast response timers via new QueuedResponse.cancel method. Remove now-unused ServerClosedError class and ERR_SERVER_CLOSED constant (neither was part of the public API).

♻️ Current situation

Responder.shutdown() calls currentProber.cancel() which calls clearTimeout(), but if the timer callback is already dequeued, clearTimeout can't prevent it from executing. The callback then calls MDNSServer.sendQueryBroadcast(), which
synchronously calls assertBeforeSend() and throws ServerClosedError. Since sendProbeRequest runs inside setTimeout callbacks with
no try/catch, this becomes an uncaught exception.

The same race exists for delayed multicast responses in the Responder: shutdown() closes the server but never cancels the
QueuedResponse timers, so their callbacks can fire after the server is closed and throw ServerClosedError from sendResponse.

The Responder had a try/catch for the delayed response case, but the Prober had no protection at all.

This probably isn't very important in production, since it happens on the shutdown path. However, it was causing spurious test failures in some code I'm working on.

💡 Proposed solution

Fix at two levels:

  1. MDNSServer (root cause): sendResponse, sendOnAllNetworksForService, and send now check this.closed and return gracefully
    instead of throwing. This makes the server safe to call during shutdown regardless of timing, and removes the need for callers to
    catch ServerClosedError.
  2. Responder.shutdown (cleanup): Cancel all pending delayed multicast response timers during shutdown, alongside the existing
    prober and broadcast interval cancellation. This prevents unnecessary work after shutdown, not just errors. A new
    QueuedResponse.cancel() method supports this.

ServerClosedError and ERR_SERVER_CLOSED are removed; neither were exposed in the API.

⚙️ Release Notes

Fixes an uncaught ERR_SERVER_CLOSED exception that could occur during shutdown when timer
callbacks raced with server teardown.

➕ Additional Information

Testing

Added QueuedResponse.spec.ts with a test that schedules a response callback, cancels it, and verifies the callback never fires.
This directly tests the new cancel() method that the shutdown cleanup relies on.

shields and others added 5 commits February 14, 2026 20:33
MDNSServer.sendResponse, sendOnAllNetworksForService, and send now
gracefully handle the closed state instead of throwing ServerClosedError.
Responder.shutdown cancels pending delayed multicast response timers via
new QueuedResponse.cancel method. Remove now-unused ServerClosedError
class and ERR_SERVER_CLOSED constant (neither was part of the public API).
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 26105798958

Coverage remained the same at 36.493%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 3134
Covered Lines: 1287
Line Coverage: 41.07%
Relevant Branches: 1656
Covered Branches: 461
Branch Coverage: 27.84%
Branches in Coverage %: Yes
Coverage Strength: 19.79 hits per line

💛 - Coveralls

@shields
Copy link
Copy Markdown
Contributor Author

shields commented May 19, 2026

No diff because 5c80603 was merged into 1.3.7. Confusing, but it worked out.

@shields shields closed this May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants