1010 * legacy `(IncomingMessage, ServerResponse)` signature — NOT the Web standard
1111 * `(Request) → Response` format.
1212 *
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`.
13+ * We use `handle()` from `@hono/node-server/vercel` which is the standard
14+ * Vercel adapter for Hono. It internally uses `getRequestListener()` to
15+ * convert `IncomingMessage → Request` (including Vercel's pre-buffered
16+ * `rawBody`) and writes the `Response` back to `ServerResponse`.
1617 *
17- * For POST/PUT/PATCH requests, Vercel pre-buffers the body on the
18- * IncomingMessage as `rawBody` (Buffer) or `body` (parsed). We extract it
19- * directly and build a clean `Request` so the inner Hono app receives a
20- * body it can `.json()` without depending on Node stream-to-ReadableStream
21- * conversion (which can hang when the stream has already been consumed).
18+ * The outer Hono app delegates all requests to the inner ObjectStack Hono
19+ * app via `inner.fetch(c.req.raw)`, matching the pattern documented in
20+ * the ObjectStack deployment guide and validated by the hono adapter tests.
2221 *
2322 * All kernel/service initialisation is co-located here so there are no
2423 * extensionless relative module imports — which would break Node's ESM
@@ -34,8 +33,8 @@ import { SecurityPlugin } from '@objectstack/plugin-security';
3433import { AuditPlugin } from '@objectstack/plugin-audit' ;
3534import { FeedServicePlugin } from '@objectstack/service-feed' ;
3635import { MetadataPlugin } from '@objectstack/metadata' ;
37- import { getRequestListener } from '@hono/node-server' ;
38- import type { Hono } from 'hono' ;
36+ import { handle } from '@hono/node-server/vercel ' ;
37+ import { Hono } from 'hono' ;
3938import { createBrokerShim } from '../src/lib/create-broker-shim.js' ;
4039import studioConfig from '../objectstack.config.js' ;
4140
@@ -198,93 +197,41 @@ async function ensureApp(): Promise<Hono> {
198197 return _app ;
199198}
200199
201- // ---------------------------------------------------------------------------
202- // Body extraction helpers
203- // ---------------------------------------------------------------------------
204-
205- /**
206- * Extract the request body from the Vercel IncomingMessage.
207- *
208- * Vercel's Node.js runtime pre-buffers the full request body and attaches it
209- * to the IncomingMessage as `rawBody` (Buffer) and/or `body` (parsed).
210- * Reading from these properties is synchronous and avoids the fragile
211- * IncomingMessage → ReadableStream conversion that can hang when the
212- * underlying Node stream has already been consumed.
213- *
214- * Returns `null` for GET/HEAD/OPTIONS or when no body is available.
215- */
216- function extractBody ( incoming : any , method : string , contentType : string | undefined ) : BodyInit | null {
217- if ( method === 'GET' || method === 'HEAD' || method === 'OPTIONS' ) {
218- return null ;
219- }
220-
221- // 1. rawBody (Buffer or string) — most reliable, set by Vercel runtime
222- if ( incoming ?. rawBody != null ) {
223- if ( typeof incoming . rawBody === 'string' ) return incoming . rawBody ;
224- if ( typeof incoming . rawBody . toString === 'function' ) return incoming . rawBody ;
225- return String ( incoming . rawBody ) ;
226- }
227-
228- // 2. body (parsed by Vercel) — re-serialize based on content-type
229- if ( incoming ?. body != null ) {
230- if ( typeof incoming . body === 'string' ) return incoming . body ;
231- if ( contentType ?. includes ( 'application/json' ) ) return JSON . stringify ( incoming . body ) ;
232- return String ( incoming . body ) ;
233- }
234-
235- return null ;
236- }
237-
238200// ---------------------------------------------------------------------------
239201// Vercel handler
240202// ---------------------------------------------------------------------------
241203
242204/**
243- * `getRequestListener` from `@hono/node-server` converts
244- * `IncomingMessage → Request`, calls our fetch callback, then writes the
245- * `Response` back to `ServerResponse` (including `res.end()`).
205+ * Outer Hono app — delegates all requests to the inner ObjectStack app.
206+ *
207+ * `handle()` from `@hono/node-server/vercel` wraps any Hono app and returns
208+ * the `(IncomingMessage, ServerResponse) => Promise<void>` signature that
209+ * Vercel's Node.js runtime expects for serverless functions. Internally it
210+ * uses `getRequestListener()`, which already handles Vercel's pre-buffered
211+ * `rawBody` (Buffer) on the IncomingMessage for POST/PUT/PATCH requests.
246212 *
247- * For requests with a body, we extract it from the IncomingMessage directly
248- * (bypassing the Node stream → ReadableStream conversion) and create a new
249- * Request that the inner Hono app can safely `.json()` .
213+ * The outer→inner delegation pattern (`inner.fetch(c.req.raw)`) is the
214+ * standard ObjectStack Vercel deployment pattern documented in the deployment
215+ * guide and covered by the @objectstack/hono adapter test suite .
250216 */
251- export default getRequestListener ( async ( request , env ) => {
252- const method = request . method ;
253- const url = request . url ;
217+ const app = new Hono ( ) ;
254218
255- console . log ( `[Vercel] ${ method } ${ url } ` ) ;
219+ app . all ( '*' , async ( c ) => {
220+ console . log ( `[Vercel] ${ c . req . method } ${ c . req . url } ` ) ;
256221
257222 try {
258- const app = await ensureApp ( ) ;
259- const incoming = ( env as any ) ?. incoming ;
260-
261- // For body methods, extract body from IncomingMessage and build a clean Request
262- if ( method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && incoming ) {
263- const contentType = incoming . headers ?. [ 'content-type' ] ;
264- const body = extractBody ( incoming , method , contentType ) ;
265-
266- if ( body != null ) {
267- console . log ( `[Vercel] Body extracted from IncomingMessage (${ typeof body === 'string' ? body . length + ' chars' : 'Buffer' } )` ) ;
268- const newReq = new Request ( url , {
269- method,
270- headers : request . headers ,
271- body,
272- } ) ;
273- return await app . fetch ( newReq ) ;
274- }
275-
276- console . log ( '[Vercel] No rawBody/body on IncomingMessage — using proxy request' ) ;
277- }
278-
279- return await app . fetch ( request ) ;
223+ const inner = await ensureApp ( ) ;
224+ return await inner . fetch ( c . req . raw ) ;
280225 } catch ( err : any ) {
281226 console . error ( '[Vercel] Handler error:' , err ?. message || err ) ;
282- return new Response (
283- JSON . stringify ( {
227+ return c . json (
228+ {
284229 success : false ,
285230 error : { message : err ?. message || 'Internal Server Error' , code : 500 } ,
286- } ) ,
287- { status : 500 , headers : { 'Content-Type' : 'application/json' } } ,
231+ } ,
232+ 500 ,
288233 ) ;
289234 }
290235} ) ;
236+
237+ export default handle ( app ) ;
0 commit comments