1212 *
1313 * We use `getRequestListener()` from `@hono/node-server` which properly
1414 * 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.
15+ * the `Response` back to `ServerResponse`.
16+ *
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).
1922 *
2023 * All kernel/service initialisation is co-located here so there are no
2124 * extensionless relative module imports — which would break Node's ESM
@@ -155,6 +158,43 @@ async function ensureApp(): Promise<Hono> {
155158 return _app ;
156159}
157160
161+ // ---------------------------------------------------------------------------
162+ // Body extraction helpers
163+ // ---------------------------------------------------------------------------
164+
165+ /**
166+ * Extract the request body from the Vercel IncomingMessage.
167+ *
168+ * Vercel's Node.js runtime pre-buffers the full request body and attaches it
169+ * to the IncomingMessage as `rawBody` (Buffer) and/or `body` (parsed).
170+ * Reading from these properties is synchronous and avoids the fragile
171+ * IncomingMessage → ReadableStream conversion that can hang when the
172+ * underlying Node stream has already been consumed.
173+ *
174+ * Returns `null` for GET/HEAD/OPTIONS or when no body is available.
175+ */
176+ function extractBody ( incoming : any , method : string , contentType : string | undefined ) : BodyInit | null {
177+ if ( method === 'GET' || method === 'HEAD' || method === 'OPTIONS' ) {
178+ return null ;
179+ }
180+
181+ // 1. rawBody (Buffer or string) — most reliable, set by Vercel runtime
182+ if ( incoming ?. rawBody != null ) {
183+ if ( typeof incoming . rawBody === 'string' ) return incoming . rawBody ;
184+ if ( typeof incoming . rawBody . toString === 'function' ) return incoming . rawBody ;
185+ return String ( incoming . rawBody ) ;
186+ }
187+
188+ // 2. body (parsed by Vercel) — re-serialize based on content-type
189+ if ( incoming ?. body != null ) {
190+ if ( typeof incoming . body === 'string' ) return incoming . body ;
191+ if ( contentType ?. includes ( 'application/json' ) ) return JSON . stringify ( incoming . body ) ;
192+ return String ( incoming . body ) ;
193+ }
194+
195+ return null ;
196+ }
197+
158198// ---------------------------------------------------------------------------
159199// Vercel handler
160200// ---------------------------------------------------------------------------
@@ -164,14 +204,38 @@ async function ensureApp(): Promise<Hono> {
164204 * `IncomingMessage → Request`, calls our fetch callback, then writes the
165205 * `Response` back to `ServerResponse` (including `res.end()`).
166206 *
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.
207+ * For requests with a body, we extract it from the IncomingMessage directly
208+ * (bypassing the Node stream → ReadableStream conversion) and create a new
209+ * Request that the inner Hono app can safely `.json()`.
171210 */
172- export default getRequestListener ( async ( request ) => {
211+ export default getRequestListener ( async ( request , env ) => {
212+ const method = request . method ;
213+ const url = request . url ;
214+
215+ console . log ( `[Vercel] ${ method } ${ url } ` ) ;
216+
173217 try {
174218 const app = await ensureApp ( ) ;
219+ const incoming = ( env as any ) ?. incoming ;
220+
221+ // For body methods, extract body from IncomingMessage and build a clean Request
222+ if ( method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && incoming ) {
223+ const contentType = incoming . headers ?. [ 'content-type' ] ;
224+ const body = extractBody ( incoming , method , contentType ) ;
225+
226+ if ( body != null ) {
227+ console . log ( `[Vercel] Body extracted from IncomingMessage (${ typeof body === 'string' ? body . length + ' chars' : 'Buffer' } )` ) ;
228+ const newReq = new Request ( url , {
229+ method,
230+ headers : request . headers ,
231+ body,
232+ } ) ;
233+ return await app . fetch ( newReq ) ;
234+ }
235+
236+ console . log ( '[Vercel] No rawBody/body on IncomingMessage — using proxy request' ) ;
237+ }
238+
175239 return await app . fetch ( request ) ;
176240 } catch ( err : any ) {
177241 console . error ( '[Vercel] Handler error:' , err ?. message || err ) ;
@@ -184,4 +248,3 @@ export default getRequestListener(async (request) => {
184248 ) ;
185249 }
186250} ) ;
187-
0 commit comments