88 *
99 * IMPORTANT: Vercel's Node.js runtime calls serverless functions with the
1010 * legacy `(IncomingMessage, ServerResponse)` signature — NOT the Web standard
11- * `(Request) → Response` format. Using `handle()` from `hono/vercel` (Edge
12- * adapter) would return a `Response` that nobody reads, while `res.end()` is
13- * never called, causing a 300-second timeout.
11+ * `(Request) → Response` format.
1412 *
15- * We use `handle()` from `@hono/node-server/vercel` instead, which properly
16- * converts `IncomingMessage → Request`, calls `app.fetch()`, then writes the
17- * `Response` back to `ServerResponse`.
13+ * We use `getRequestListener()` from `@hono/node-server` which properly
14+ * converts `IncomingMessage → Request`, calls our fetch callback, then writes
15+ * the `Response` back to `ServerResponse`. The fetch callback is called
16+ * directly (no outer Hono app relay) so POST body streams are consumed by
17+ * exactly one Hono instance — avoiding the body-lock hang that occurs when
18+ * an intermediate Hono app reads `c.req.raw` before forwarding.
1819 *
1920 * All kernel/service initialisation is co-located here so there are no
2021 * extensionless relative module imports — which would break Node's ESM
@@ -25,8 +26,8 @@ import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
2526import { ObjectQLPlugin } from '@objectstack/objectql' ;
2627import { InMemoryDriver } from '@objectstack/driver-memory' ;
2728import { createHonoApp } from '@objectstack/hono' ;
28- import { Hono } from 'hono' ;
29- import { handle } from '@ hono/node-server/vercel ' ;
29+ import { getRequestListener } from '@ hono/node-server ' ;
30+ import type { Hono } from 'hono' ;
3031import { createBrokerShim } from '../src/lib/create-broker-shim.js' ;
3132import studioConfig from '../objectstack.config.js' ;
3233
@@ -158,32 +159,29 @@ async function ensureApp(): Promise<Hono> {
158159// Vercel handler
159160// ---------------------------------------------------------------------------
160161
161- const app = new Hono ( ) ;
162-
163162/**
164- * Delegate every request to the lazily-initialized ObjectStack Hono app.
165- * `ensureApp()` boots the kernel on the first invocation (cold start)
166- * and returns the cached instance on subsequent warm invocations.
163+ * `getRequestListener` from `@hono/node-server` converts
164+ * `IncomingMessage → Request`, calls our fetch callback, then writes the
165+ * `Response` back to `ServerResponse` (including `res.end()`).
166+ *
167+ * By calling `ensureApp()` inside the fetch callback we get lazy kernel
168+ * boot AND the Request (with its body stream) is handed directly to the
169+ * ObjectStack Hono app — no intermediate Hono relay that would lock the
170+ * body stream before the real handler can read it.
167171 */
168- app . all ( '*' , async ( c ) => {
172+ export default getRequestListener ( async ( request ) => {
169173 try {
170- const inner = await ensureApp ( ) ;
171- return await inner . fetch ( c . req . raw ) ;
174+ const app = await ensureApp ( ) ;
175+ return await app . fetch ( request ) ;
172176 } catch ( err : any ) {
173177 console . error ( '[Vercel] Handler error:' , err ?. message || err ) ;
174- return c . json (
175- { success : false , error : { message : err ?. message || 'Internal Server Error' , code : 500 } } ,
176- 500 ,
178+ return new Response (
179+ JSON . stringify ( {
180+ success : false ,
181+ error : { message : err ?. message || 'Internal Server Error' , code : 500 } ,
182+ } ) ,
183+ { status : 500 , headers : { 'Content-Type' : 'application/json' } } ,
177184 ) ;
178185 }
179186} ) ;
180187
181- /**
182- * `handle()` from `@hono/node-server/vercel` returns an
183- * `async (IncomingMessage, ServerResponse) => void` handler that:
184- * 1. Converts IncomingMessage → standard Request
185- * 2. Calls app.fetch(request)
186- * 3. Writes the Response back to ServerResponse (including res.end())
187- */
188- export default handle ( app ) ;
189-
0 commit comments