Skip to content

Commit 5d8742d

Browse files
committed
fix(server-core): report occupied initial port
1 parent 28ebcfc commit 5d8742d

3 files changed

Lines changed: 110 additions & 32 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@voltagent/server-core": patch
3+
---
4+
5+
fix(server-core): report initial port conflicts instead of silently switching ports
6+
7+
When the requested or default port is already in use, VoltAgent now stops with guidance for configuring a different port instead of automatically binding to another available port. This includes the `honoServer()` default path: calling `honoServer()` without a port now fails fast if the default `3141` port is occupied, and users can opt into another port with `honoServer({ port: 4310 })`.

packages/server-core/src/utils/port-manager.spec.ts

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,12 @@ describe("PortManager", () => {
106106
}
107107
});
108108

109-
// Try to allocate again - should get next port
110-
const port2 = await portManager.allocatePort(3141);
111-
expect(port2).not.toBe(3141);
112-
expect(port2).toBe(4310); // Next preferred port
109+
await expect(portManager.allocatePort(3141)).rejects.toThrow(
110+
"Port 3141 is already in use. To use another port, pass it to your server config, for example: honoServer({ port: 4310 })",
111+
);
113112
});
114113

115-
it("should handle port in use (EADDRINUSE)", async () => {
114+
it("should throw with guidance when preferred port is in use (EADDRINUSE)", async () => {
116115
let portBeingTested: number | undefined;
117116

118117
(createNetServer as any).mockImplementation(() => {
@@ -145,9 +144,44 @@ describe("PortManager", () => {
145144
return mockServer;
146145
});
147146

148-
const port = await portManager.allocatePort(3141);
149-
expect(port).not.toBe(3141);
150-
expect(port).toBe(4310); // Should try next port after skipping duplicate 3141
147+
await expect(portManager.allocatePort(3141)).rejects.toThrow(
148+
"Port 3141 is already in use. To use another port, pass it to your server config, for example: honoServer({ port: 4310 })",
149+
);
150+
});
151+
152+
it("should throw with guidance when default port is in use", async () => {
153+
let portBeingTested: number | undefined;
154+
155+
(createNetServer as any).mockImplementation(() => {
156+
const mockServer = {
157+
once: vi.fn(),
158+
listen: vi.fn((port: number) => {
159+
portBeingTested = port;
160+
}),
161+
close: vi.fn((callback?: () => void) => callback?.()),
162+
};
163+
164+
const handlers: Record<string, (...args: any[]) => void> = {};
165+
mockServer.once.mockImplementation((event: string, handler: (...args: any[]) => void) => {
166+
handlers[event] = handler;
167+
if (Object.keys(handlers).length === 2) {
168+
setTimeout(() => {
169+
if (portBeingTested === 3141) {
170+
handlers.error({ code: "EADDRINUSE" });
171+
} else {
172+
handlers.listening();
173+
}
174+
}, 0);
175+
}
176+
return mockServer;
177+
});
178+
179+
return mockServer;
180+
});
181+
182+
await expect(portManager.allocatePort()).rejects.toThrow(
183+
"Port 3141 is already in use. To use another port, pass it to your server config, for example: honoServer({ port: 4310 })",
184+
);
151185
});
152186

153187
it("should handle permission denied (EACCES)", async () => {
@@ -183,9 +217,9 @@ describe("PortManager", () => {
183217
return mockServer;
184218
});
185219

186-
const port = await portManager.allocatePort(80); // Low port, likely EACCES
187-
expect(port).not.toBe(80);
188-
expect(port).toBe(3141); // Should try next port
220+
await expect(portManager.allocatePort(80)).rejects.toThrow(
221+
"Port 80 is already in use. To use another port, pass it to your server config, for example: honoServer({ port: 3141 })",
222+
);
189223
});
190224

191225
it("should throw error when no ports are available", async () => {
@@ -202,10 +236,12 @@ describe("PortManager", () => {
202236
getPortsToTry: originalGetPortsToTry,
203237
}));
204238

205-
await expect(portManager.allocatePort()).rejects.toThrow("Could not find an available port");
239+
await expect(portManager.allocatePort()).rejects.toThrow(
240+
"Port 3141 is already in use and no alternative port is available",
241+
);
206242
});
207243

208-
it("should handle concurrent allocation requests", async () => {
244+
it("should reject concurrent default allocation requests after the first allocation", async () => {
209245
// Create multiple mock servers for concurrent allocation
210246
(createNetServer as any).mockImplementation(() => {
211247
const newMockServer = {
@@ -226,21 +262,28 @@ describe("PortManager", () => {
226262
});
227263

228264
// Start multiple allocations concurrently
229-
const promises = [
265+
const results = await Promise.allSettled([
230266
portManager.allocatePort(),
231267
portManager.allocatePort(),
232268
portManager.allocatePort(),
233-
];
269+
]);
234270

235-
const results = await Promise.all(promises);
271+
const fulfilled = results.filter((result) => result.status === "fulfilled");
272+
const rejected = results.filter((result) => result.status === "rejected");
236273

237-
// All ports should be different
238-
expect(new Set(results).size).toBe(3);
274+
expect(fulfilled).toHaveLength(1);
275+
expect(rejected).toHaveLength(2);
276+
expect(fulfilled[0]).toMatchObject({ value: 3141 });
239277

240-
// All ports should be allocated
241-
results.forEach((port) => {
242-
expect(portManager.isPortAllocated(port)).toBe(true);
278+
rejected.forEach((result) => {
279+
expect(result.reason).toBeInstanceOf(Error);
280+
expect((result.reason as Error).message).toMatch(
281+
/^Port 3141 is already in use\. To use another port, pass it to your server config, for example: honoServer\(\{ port: \d+ \}\)$/,
282+
);
243283
});
284+
285+
expect(portManager.isPortAllocated(3141)).toBe(true);
286+
expect(portManager.isPortAllocated(4310)).toBe(false);
244287
});
245288

246289
it("should wait for existing port check to complete", async () => {
@@ -269,12 +312,13 @@ describe("PortManager", () => {
269312
await delay(10); // Small delay to ensure first check is in progress
270313
const promise2 = portManager.allocatePort(5000);
271314

272-
const [port1, port2] = await Promise.all([promise1, promise2]);
315+
const port1 = await promise1;
316+
await expect(promise2).rejects.toThrow(
317+
"Port 5000 is already in use. To use another port, pass it to your server config, for example: honoServer({ port: 3141 })",
318+
);
273319

274320
// First should get the requested port
275321
expect(port1).toBe(5000);
276-
// Second should get a different port (since first is checking/allocated)
277-
expect(port2).not.toBe(5000);
278322
expect(checkCompleted).toBe(true);
279323
});
280324
});
@@ -443,9 +487,9 @@ describe("PortManager", () => {
443487
return mockServer;
444488
});
445489

446-
const port = await portManager.allocatePort(3000);
447-
expect(port).not.toBe(3000);
448-
expect(port).toBe(3141); // Should try the first preferred port
490+
await expect(portManager.allocatePort(3000)).rejects.toThrow(
491+
"Port 3000 is already in use. To use another port, pass it to your server config, for example: honoServer({ port: 3141 })",
492+
);
449493
});
450494

451495
it("should cleanup promises map even on error", async () => {

packages/server-core/src/utils/port-manager.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,49 @@ class PortManager {
8585
*/
8686
public async allocatePort(preferredPort?: number): Promise<number> {
8787
const portsToTry = getPortsToTry(preferredPort);
88+
const initialPort = portsToTry[0];
89+
90+
if (!this.allocatedPorts.has(initialPort)) {
91+
const isAvailable = await this.isPortAvailable(initialPort);
92+
if (isAvailable) {
93+
this.allocatedPorts.add(initialPort);
94+
return initialPort;
95+
}
96+
}
97+
98+
const alternativePort = await this.findAvailableAlternativePort(initialPort, portsToTry);
99+
throw new Error(this.createPortInUseMessage(initialPort, alternativePort));
100+
}
101+
102+
private async findAvailableAlternativePort(
103+
unavailablePort: number,
104+
portsToTry: number[],
105+
): Promise<number> {
106+
const seenPorts = new Set<number>([unavailablePort]);
88107

89108
for (const port of portsToTry) {
90-
// Skip if already allocated
91-
if (this.allocatedPorts.has(port)) {
109+
if (seenPorts.has(port) || this.allocatedPorts.has(port)) {
92110
continue;
93111
}
94112

113+
seenPorts.add(port);
114+
95115
const isAvailable = await this.isPortAvailable(port);
96116
if (isAvailable) {
97-
// Reserve this port
98-
this.allocatedPorts.add(port);
99117
return port;
100118
}
101119
}
102120

103-
throw new Error("Could not find an available port");
121+
throw new Error(
122+
`Port ${unavailablePort} is already in use and no alternative port is available`,
123+
);
124+
}
125+
126+
private createPortInUseMessage(unavailablePort: number, alternativePort: number): string {
127+
return [
128+
`Port ${unavailablePort} is already in use.`,
129+
`To use another port, pass it to your server config, for example: honoServer({ port: ${alternativePort} })`,
130+
].join(" ");
104131
}
105132

106133
/**

0 commit comments

Comments
 (0)