From e5a717ec66f5be8a8f386ef47fd43bac90efcdc1 Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Wed, 5 Aug 2026 11:28:04 +0200 Subject: [PATCH 1/2] feat(devframe): add onServerError for post-bind server errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit startHttpAndWs's owned httpServer has no error listener once past the bind window, so a later runtime error (e.g. a transient EMFILE while accepting a connection) crashes the process. StartedServer exposes no handle a caller could attach their own listener to either, unlike the shared-server path where the caller already owns the object. Add onServerError instead of exposing the raw httpServer — handing out the raw object would let a caller call close() on it directly, bypassing the wrapper's own close() and leaking the WS transport. Attached only after the bind has already succeeded, so this never touches bind-time crash/hang semantics — only what happens afterward. No test: StartedServer deliberately doesn't expose the raw server, so there's no way to trigger a genuine post-bind error through the public API without a test-only seam. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- packages/devframe/src/node/server.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/devframe/src/node/server.ts b/packages/devframe/src/node/server.ts index 17c53854..2036161d 100644 --- a/packages/devframe/src/node/server.ts +++ b/packages/devframe/src/node/server.ts @@ -110,6 +110,14 @@ export interface StartHttpAndWsOptions { * own startup banner. Devframe does not print one itself. */ onReady?: (info: { origin: string, port: number, app: H3 }) => void | Promise + /** + * Called for any error the owned HTTP server emits after it starts + * listening — e.g. a transient `EMFILE` while accepting a connection. + * Ignored when a `server` is supplied — the caller already owns that + * object and can listen on it directly. Without this, a post-bind error + * has no listener and crashes the process. + */ + onServerError?: (error: Error) => void } export interface StartedServer { @@ -258,6 +266,10 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise((resolveListen) => { httpServer.listen(port, bindHost, () => resolveListen()) }) + // Attached only now that the bind has already succeeded — this never + // changes bind-time crash/hang semantics, only what happens afterward. + if (options.onServerError) + httpServer.on('error', options.onServerError) } const address = httpServer.address() From 8fe156417aa68bb9edf768a180664e6cdbe8c645 Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Wed, 5 Aug 2026 11:47:46 +0200 Subject: [PATCH 2/2] docs(devframe): clarify onServerError applies to owned servers only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The option silently does nothing when a `server` is supplied, so lead the JSDoc with that constraint (and why — the caller owns that object and its crash semantics are not devframe's to change) instead of mentioning it mid-paragraph. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- packages/devframe/src/node/server.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/devframe/src/node/server.ts b/packages/devframe/src/node/server.ts index 2036161d..5dfc8db6 100644 --- a/packages/devframe/src/node/server.ts +++ b/packages/devframe/src/node/server.ts @@ -111,11 +111,13 @@ export interface StartHttpAndWsOptions { */ onReady?: (info: { origin: string, port: number, app: H3 }) => void | Promise /** - * Called for any error the owned HTTP server emits after it starts + * Called for any error the HTTP server devframe owns emits after it starts * listening — e.g. a transient `EMFILE` while accepting a connection. - * Ignored when a `server` is supplied — the caller already owns that - * object and can listen on it directly. Without this, a post-bind error - * has no listener and crashes the process. + * Without it such an error has no listener and crashes the process. + * + * Applies only to a server devframe created itself. When `server` is + * supplied the caller owns that object and attaches to it directly, so + * devframe leaves its error handling alone. */ onServerError?: (error: Error) => void }