Skip to content

Commit d93089d

Browse files
committed
fix: enhance body extraction from Vercel IncomingMessage for improved request handling
1 parent 68990e9 commit d93089d

File tree

1 file changed

+73
-10
lines changed

1 file changed

+73
-10
lines changed

apps/studio/api/index.ts

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
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

Comments
 (0)