fix(studio): use getRequestListener() with extractBody() for Vercel POST support#1066
fix(studio): use getRequestListener() with extractBody() for Vercel POST support#1066
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…timeout The outer→inner Hono app delegation pattern passed the @hono/node-server pseudo-Request directly to the inner app. The pseudo-Request lazily materialises its body from the Node.js IncomingMessage via Readable.toWeb() which can hang on Vercel's serverless runtime, causing POST/PUT/PATCH requests to time out while GET requests work fine. Fix: for methods with a body, eagerly buffer via arrayBuffer() while the IncomingMessage is in a known-good state, then create a plain Request for the inner app. GET/HEAD continue to use the direct pass-through. Also adds POST/PUT/PATCH/DELETE body forwarding tests for the Vercel delegation pattern (previously only GET was tested). Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/15d6a2bf-9777-42de-92d4-56a61b26dc77 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…OST support Replace the `handle()` + outer Hono app delegation pattern with `getRequestListener()` from `@hono/node-server`, matching the proven pattern from the hotcrm reference deployment. The previous approach used `handle()` from `@hono/node-server/vercel` wrapped in an outer Hono app that delegated via `inner.fetch(c.req.raw)`. On Vercel the IncomingMessage stream is already drained by the time the inner app reads it, causing POST/PUT/PATCH requests to hang indefinitely. The new approach uses `getRequestListener()` directly which exposes the raw IncomingMessage via `env.incoming`. For POST/PUT/PATCH requests the body is extracted from Vercel's pre-buffered `rawBody`/`body` properties and a fresh standard `Request` is constructed. Also adds x-forwarded-proto URL correction for proper HTTPS detection behind Vercel's reverse proxy. Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/8923ba52-78fa-4a0c-8f11-edd6b20e611c Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ent Request creation Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/8923ba52-78fa-4a0c-8f11-edd6b20e611c Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Rewrote the Vercel handler to match the hotcrm pattern in Replaced |
There was a problem hiding this comment.
Pull request overview
Fixes Studio’s Vercel serverless handler so POST/PUT/PATCH requests no longer hang by avoiding delegation with a drained IncomingMessage stream and instead forwarding a fresh Request built from Vercel’s pre-buffered body.
Changes:
- Replaced
@hono/node-server/vercelhandle()+ outer→inner delegation withgetRequestListener()and anextractBody()-based forwarding path for non-GET methods. - Added adapter tests covering body forwarding for POST/PUT/PATCH (and DELETE) through outer→inner delegation, including a buffered-body delegation helper.
- Documented the Vercel fix in
apps/studio/CHANGELOG.md.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| apps/studio/server/index.ts | Implements Vercel-safe request handling via getRequestListener(), extractBody(), and public URL protocol correction. |
| packages/adapters/hono/src/hono.test.ts | Adds regression tests for request body forwarding through delegation patterns (including buffered forwarding). |
| apps/studio/CHANGELOG.md | Records the Vercel POST/PUT/PATCH timeout fix and rationale. |
| export default getRequestListener(async (request, env) => { | ||
| let app: Hono; | ||
| try { | ||
| const inner = await ensureApp(); | ||
| return await inner.fetch(c.req.raw); | ||
| } catch (err: any) { | ||
| console.error('[Vercel] Handler error:', err?.message || err); | ||
| return c.json( | ||
| { | ||
| app = await ensureApp(); | ||
| } catch (err: unknown) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| console.error('[Vercel] Handler error — bootstrap did not complete:', message); |
There was a problem hiding this comment.
The handler calls ensureApp() (kernel bootstrap) before checking the HTTP method. This means CORS preflight OPTIONS requests will also trigger a cold start, and there is no fast-path response for preflight. Consider short-circuiting OPTIONS before bootstrapping (and returning the appropriate CORS headers / 204) so browsers can complete preflight without waiting for kernel init.
| if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && incoming) { | ||
| const contentType = incoming.headers?.['content-type']; | ||
| const contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType; | ||
| const body = extractBody(incoming, method, contentTypeStr); | ||
| if (body != null) { | ||
| return await app.fetch( | ||
| new Request(url, { method, headers: request.headers, body }), | ||
| ); |
There was a problem hiding this comment.
When constructing the forwarded Request, the code reuses request.headers unchanged. If the body is rebuilt from incoming.body (e.g., via JSON.stringify), headers like content-length may no longer match the forwarded payload, which can cause downstream body parsing issues. Safer approach: clone headers and remove hop-by-hop headers (at least content-length, and potentially transfer-encoding/connection) before creating the new Request.
| if (typeof incoming.body === 'string') return incoming.body; | ||
| if (contentType?.includes('application/json')) return JSON.stringify(incoming.body); | ||
| return String(incoming.body); |
There was a problem hiding this comment.
extractBody() falls back to String(incoming.body) for non-JSON content types. If incoming.body is an object (common for urlencoded/form parsing), this becomes "[object Object]" and forwards an invalid payload. Prefer forwarding rawBody whenever possible; otherwise, add explicit handling for application/x-www-form-urlencoded (e.g., URLSearchParams) and avoid lossy string coercion for structured bodies.
| if (typeof incoming.body === 'string') return incoming.body; | |
| if (contentType?.includes('application/json')) return JSON.stringify(incoming.body); | |
| return String(incoming.body); | |
| const body: unknown = incoming.body; | |
| // Already-encoded string bodies can be forwarded as-is. | |
| if (typeof body === 'string') { | |
| return body; | |
| } | |
| // Preserve binary bodies. | |
| if (typeof Buffer !== 'undefined' && Buffer.isBuffer(body)) { | |
| return body; | |
| } | |
| // Respect URLSearchParams bodies. | |
| if (body instanceof URLSearchParams) { | |
| return body; | |
| } | |
| // For form-urlencoded content, encode plain objects into a query string. | |
| if (contentType?.includes('application/x-www-form-urlencoded') && body && typeof body === 'object') { | |
| const params = new URLSearchParams(); | |
| for (const [key, value] of Object.entries(body as Record<string, unknown>)) { | |
| if (Array.isArray(value)) { | |
| for (const v of value) { | |
| if (v != null) params.append(key, String(v)); | |
| } | |
| } else if (value != null) { | |
| params.append(key, String(value)); | |
| } | |
| } | |
| return params.toString(); | |
| } | |
| // For JSON and other structured bodies, prefer JSON encoding over lossy String(). | |
| if (contentType?.includes('application/json')) { | |
| return JSON.stringify(body); | |
| } | |
| if (typeof body === 'object') { | |
| return JSON.stringify(body); | |
| } | |
| // For primitive scalar values, String() is a faithful representation. | |
| if (typeof body === 'number' || typeof body === 'boolean' || typeof body === 'bigint') { | |
| return String(body); | |
| } |
POST/PUT/PATCH API requests on Vercel-deployed Studio time out while GET works fine.
Cause
The Vercel handler used
handle()from@hono/node-server/vercelwrapped in an outer Hono app that delegated all requests to the inner ObjectStack Hono app viainner.fetch(c.req.raw). On Vercel's serverless runtime, theIncomingMessagestream is already drained by the time the handler runs, so the inner app's lazy.json()call hangs indefinitely when trying to read the consumed stream.Fix
Rewrote the Vercel handler to match the proven hotcrm reference pattern:
apps/studio/server/index.ts— Replacedhandle()+ outer Hono app delegation withgetRequestListener()from@hono/node-server, which exposes the rawIncomingMessageviaenv.incoming. For POST/PUT/PATCH requests, the body is extracted from Vercel's pre-bufferedrawBody/bodyproperties using anextractBody()helper, and a fresh standardRequestis constructed for the inner Hono app. Also addsx-forwarded-protoURL correction viaresolvePublicUrl()for proper HTTPS detection behind Vercel's reverse proxy.packages/adapters/hono/src/hono.test.ts— Added POST/PUT/PATCH/DELETE body forwarding tests for the Vercel delegation pattern (previously only GET was covered).Original prompt