Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/errors/DF0052.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
outline: deep
---

# DF0052: HTTP Server Failed to Listen

## Message

> Failed to listen on `{host}:{port}`: `{reason}`

## Cause

`startHttpAndWs` tried to bind the HTTP server it owns to `host:port` and the underlying `listen()` call failed — most commonly `EADDRINUSE` (another process, often a previous devframe instance, is already bound to that port) or `EACCES` (insufficient permissions, typically a privileged port). The WS RPC transport is torn down before this error surfaces, so nothing is leaked.

## Example

```ts
// A previous instance is still bound to 4096:
// await startHttpAndWs({ context, host: 'localhost', port: 4096 }) → DF0052
```

## Fix

- Free the port, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `devMiddleware.port` on `viteDevBridge`.
- The original node error is available as `error.cause` — check `error.cause.code` (e.g. `'EADDRINUSE'`) to branch on the failure kind programmatically.

## Source

- [`packages/devframe/src/node/server.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/server.ts) — `startHttpAndWs()` throws this when its owned HTTP server's `listen()` fails.
16 changes: 16 additions & 0 deletions packages/devframe/src/node/__tests__/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,19 @@ describe('startHttpAndWs rpcOptions passthrough', () => {
}
})
})

describe('startHttpAndWs listen failures', () => {
it('rejects when the port is already taken instead of hanging', async () => {
const host = '127.0.0.1'
const first = await startHttpAndWs({ context: await createTestContext(), host, port: 0, auth: false })

try {
await expect(
startHttpAndWs({ context: await createTestContext(), host, port: first.port, auth: false }),
).rejects.toThrow(expect.objectContaining({ code: 'DF0052' }))
}
finally {
await first.close()
}
})
})
4 changes: 4 additions & 0 deletions packages/devframe/src/node/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,9 @@ export const diagnostics = defineDiagnostics({
why: (p: { port: number }) => `The devframe instance on port ${p.port} has no MCP endpoint.`,
fix: 'Restart the instance with the --mcp flag (or set `cli.mcp: true` on its definition) to expose its tools, then list instances again.',
},
DF0052: {
why: (p: { host: string, port: number, reason: string }) => `Failed to listen on ${p.host}:${p.port}: ${p.reason}`,
fix: 'The port is likely already taken by another process (often a previous devframe instance). Free it, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `devMiddleware.port` on `viteDevBridge`. The original node error is available as `error.cause`.',
},
},
})
27 changes: 24 additions & 3 deletions packages/devframe/src/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,30 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise<St
// Only start listening on a server we created. A shared server is already
// (or about to be) listening under the caller's control.
if (ownsHttpServer) {
await new Promise<void>((resolveListen) => {
httpServer.listen(port, bindHost, () => resolveListen())
})
try {
await new Promise<void>((resolve, reject) => {
const onError = (error: Error): void => reject(error)
// Without this listener a failed bind emits `error` with nobody
// attached — an uncaughtException — and the `listen` callback never
// fires, so this promise never settles.
httpServer.once('error', onError)
httpServer.listen(port, bindHost, () => {
httpServer.removeListener('error', onError)
resolve()
})
})
}
catch (error) {
// The WS transport is already attached above, so tear it down before
// surfacing the failure rather than leaking it and its peers.
await closeWs().catch(() => {})
throw diagnostics.DF0052({
host: bindHost,
port,
reason: error instanceof Error ? error.message : String(error),
cause: error,
})
}
}

const address = httpServer.address()
Expand Down
Loading