Skip to content

Commit 5e5f1d4

Browse files
committed
fix: address race condition and missing error handler in auth-server
- Add `ready` promise to CallbackServer interface that resolves when server is actually listening, fixing race condition where getPort() could return 0 before server started - Add error handler for server 'error' event to properly handle EADDRINUSE and other binding errors instead of silent failures - Update postgres-ai.ts to await ready promise instead of fragile 100ms setTimeout workaround
1 parent 4eaa5c6 commit 5e5f1d4

2 files changed

Lines changed: 30 additions & 4 deletions

File tree

cli/bin/postgres-ai.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,9 +1622,8 @@ auth
16221622
const requestedPort = opts.port || 0; // 0 = OS assigns available port
16231623
const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 120000); // 2 minute timeout
16241624

1625-
// Wait a bit for server to start and get port
1626-
await new Promise(resolve => setTimeout(resolve, 100));
1627-
const actualPort = callbackServer.getPort();
1625+
// Wait for server to start and get the actual port
1626+
const actualPort = await callbackServer.ready;
16281627
const redirectUri = `http://localhost:${actualPort}/callback`;
16291628

16301629
console.log(`Callback server listening on port ${actualPort}`);

cli/lib/auth-server.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface CallbackResult {
1414
export interface CallbackServer {
1515
server: { stop: () => void };
1616
promise: Promise<CallbackResult>;
17+
ready: Promise<number>; // Resolves with actual port when server is listening
1718
getPort: () => number;
1819
}
1920

@@ -38,9 +39,12 @@ function escapeHtml(str: string | null): string {
3839
* @param port - Port to listen on (0 for random available port)
3940
* @param expectedState - Expected state parameter for CSRF protection
4041
* @param timeoutMs - Timeout in milliseconds
41-
* @returns Server object with promise and getPort function
42+
* @returns Server object with promise, ready promise, and getPort function
4243
*
4344
* @remarks
45+
* The `ready` promise resolves with the actual port once the server is listening.
46+
* Callers should await `ready` before using `getPort()` when using port 0.
47+
*
4448
* The server stops asynchronously ~100ms after the callback resolves/rejects.
4549
* This delay ensures the HTTP response is fully sent before closing the connection.
4650
* Callers should not attempt to reuse the same port immediately after the promise
@@ -55,13 +59,20 @@ export function createCallbackServer(
5559
let actualPort = port;
5660
let resolveCallback: (value: CallbackResult) => void;
5761
let rejectCallback: (reason: Error) => void;
62+
let resolveReady: (port: number) => void;
63+
let rejectReady: (reason: Error) => void;
5864
let serverInstance: http.Server | null = null;
5965

6066
const promise = new Promise<CallbackResult>((resolve, reject) => {
6167
resolveCallback = resolve;
6268
rejectCallback = reject;
6369
});
6470

71+
const ready = new Promise<number>((resolve, reject) => {
72+
resolveReady = resolve;
73+
rejectReady = reject;
74+
});
75+
6576
const stopServer = () => {
6677
if (serverInstance) {
6778
serverInstance.close();
@@ -223,16 +234,32 @@ export function createCallbackServer(
223234
`);
224235
});
225236

237+
// Handle server errors (e.g., EADDRINUSE)
238+
serverInstance.on("error", (err: NodeJS.ErrnoException) => {
239+
clearTimeout(timeout);
240+
if (err.code === "EADDRINUSE") {
241+
rejectReady(new Error(`Port ${port} is already in use`));
242+
} else {
243+
rejectReady(new Error(`Server error: ${err.message}`));
244+
}
245+
if (!resolved) {
246+
resolved = true;
247+
rejectCallback(err);
248+
}
249+
});
250+
226251
serverInstance.listen(port, "127.0.0.1", () => {
227252
const address = serverInstance?.address();
228253
if (address && typeof address === "object") {
229254
actualPort = address.port;
230255
}
256+
resolveReady(actualPort);
231257
});
232258

233259
return {
234260
server: { stop: stopServer },
235261
promise,
262+
ready,
236263
getPort: () => actualPort,
237264
};
238265
}

0 commit comments

Comments
 (0)