Skip to content

Commit 832f094

Browse files
authored
fix(server-core): report occupied initial port (#1247)
1 parent 28ebcfc commit 832f094

3 files changed

Lines changed: 105 additions & 21 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 requested port conflicts instead of silently switching ports
6+
7+
When a server provider is configured with an explicit port and that port is already in use, VoltAgent now stops with guidance for configuring a different port instead of automatically binding to another available port. Calls without an explicit port keep the previous automatic fallback behavior.

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

Lines changed: 54 additions & 21 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 or unavailable. Configure your server provider with a different port, for example: { 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 or unavailable. Configure your server provider with a different port, for example: { port: 4310 }",
149+
);
150+
});
151+
152+
it("should fall through to the next available port 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+
const port = await portManager.allocatePort();
183+
expect(port).toBe(4310);
184+
expect(portManager.isPortAllocated(4310)).toBe(true);
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 or unavailable. Configure your server provider with a different port, for example: { port: 3141 }",
222+
);
189223
});
190224

191225
it("should throw error when no ports are available", async () => {
@@ -226,13 +260,11 @@ describe("PortManager", () => {
226260
});
227261

228262
// Start multiple allocations concurrently
229-
const promises = [
263+
const results = await Promise.all([
230264
portManager.allocatePort(),
231265
portManager.allocatePort(),
232266
portManager.allocatePort(),
233-
];
234-
235-
const results = await Promise.all(promises);
267+
]);
236268

237269
// All ports should be different
238270
expect(new Set(results).size).toBe(3);
@@ -269,12 +301,13 @@ describe("PortManager", () => {
269301
await delay(10); // Small delay to ensure first check is in progress
270302
const promise2 = portManager.allocatePort(5000);
271303

272-
const [port1, port2] = await Promise.all([promise1, promise2]);
304+
const port1 = await promise1;
305+
await expect(promise2).rejects.toThrow(
306+
"Port 5000 is already in use or unavailable. Configure your server provider with a different port, for example: { port: 3141 }",
307+
);
273308

274309
// First should get the requested port
275310
expect(port1).toBe(5000);
276-
// Second should get a different port (since first is checking/allocated)
277-
expect(port2).not.toBe(5000);
278311
expect(checkCompleted).toBe(true);
279312
});
280313
});
@@ -443,9 +476,9 @@ describe("PortManager", () => {
443476
return mockServer;
444477
});
445478

446-
const port = await portManager.allocatePort(3000);
447-
expect(port).not.toBe(3000);
448-
expect(port).toBe(3141); // Should try the first preferred port
479+
await expect(portManager.allocatePort(3000)).rejects.toThrow(
480+
"Port 3000 is already in use or unavailable. Configure your server provider with a different port, for example: { port: 3141 }",
481+
);
449482
});
450483

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

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ class PortManager {
8686
public async allocatePort(preferredPort?: number): Promise<number> {
8787
const portsToTry = getPortsToTry(preferredPort);
8888

89+
if (preferredPort !== undefined) {
90+
if (!this.allocatedPorts.has(preferredPort)) {
91+
const isAvailable = await this.isPortAvailable(preferredPort);
92+
if (isAvailable) {
93+
this.allocatedPorts.add(preferredPort);
94+
return preferredPort;
95+
}
96+
}
97+
98+
const alternativePort = await this.findAvailableAlternativePort(preferredPort, portsToTry);
99+
throw new Error(this.createPortInUseMessage(preferredPort, alternativePort));
100+
}
101+
89102
for (const port of portsToTry) {
90103
// Skip if already allocated
91104
if (this.allocatedPorts.has(port)) {
@@ -103,6 +116,37 @@ class PortManager {
103116
throw new Error("Could not find an available port");
104117
}
105118

119+
private async findAvailableAlternativePort(
120+
unavailablePort: number,
121+
portsToTry: number[],
122+
): Promise<number> {
123+
const seenPorts = new Set<number>([unavailablePort]);
124+
125+
for (const port of portsToTry) {
126+
if (seenPorts.has(port) || this.allocatedPorts.has(port)) {
127+
continue;
128+
}
129+
130+
seenPorts.add(port);
131+
132+
const isAvailable = await this.isPortAvailable(port);
133+
if (isAvailable) {
134+
return port;
135+
}
136+
}
137+
138+
throw new Error(
139+
`Port ${unavailablePort} is already in use and no alternative port is available`,
140+
);
141+
}
142+
143+
private createPortInUseMessage(unavailablePort: number, alternativePort: number): string {
144+
return [
145+
`Port ${unavailablePort} is already in use or unavailable.`,
146+
`Configure your server provider with a different port, for example: { port: ${alternativePort} }`,
147+
].join(" ");
148+
}
149+
106150
/**
107151
* Release a previously allocated port
108152
* @param port The port number to release

0 commit comments

Comments
 (0)