Skip to content

fix: UIServer.close() should await HTTP server shutdown #12757

@jeffspahr

Description

@jeffspahr

Problem

UIServer.close() calls this.httpServer.close() but doesn't wait for the server to actually finish closing:

// app.ts:86
close() {
  if (this.httpServer) {
    this.httpServer.close();  // async, but not awaited
  }
  this.httpServer = undefined;
  return this;
}

http.Server.close() is asynchronous — the OS doesn't release the port immediately. This causes EADDRINUSE errors in tests when tests run back-to-back, because the previous server's port hasn't been released yet.

Current Workaround

Tests now use supertest(app.app) (passing the Express application directly) instead of supertest(app.start()) (passing the listening HTTP server). This lets supertest create ephemeral servers on random ports, avoiding the port conflict entirely.

Suggested Fix

Make UIServer.close() return a Promise that resolves when the server is fully closed:

close(): Promise<void> {
  return new Promise((resolve, reject) => {
    if (this.httpServer) {
      this.httpServer.close((err) => {
        this.httpServer = undefined;
        if (err) reject(err);
        else resolve();
      });
    } else {
      resolve();
    }
  });
}

This would also benefit production graceful shutdown scenarios.

Context

Discovered during the Jest → Vitest migration (PR #12756). With done callbacks, the timing gap between tests masked the issue. With async/await, tests chain faster and expose the race condition.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions