Skip to content

fix(studio): use getRequestListener() with extractBody() for Vercel POST support - #1066

Merged
hotlong merged 4 commits into
mainfrom
copilot/fix-post-api-timeout
Apr 2, 2026
Merged

fix(studio): use getRequestListener() with extractBody() for Vercel POST support#1066
hotlong merged 4 commits into
mainfrom
copilot/fix-post-api-timeout

Conversation

CopilotAI commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

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/vercel wrapped in an outer Hono app that delegated all requests to the inner ObjectStack Hono app via inner.fetch(c.req.raw). On Vercel's serverless runtime, the IncomingMessage stream 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 — Replaced handle() + outer Hono app delegation with getRequestListener() from @hono/node-server, 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 using an extractBody() helper, and a fresh standard Request is constructed for the inner Hono app. Also adds x-forwarded-proto URL correction via resolvePublicUrl() for proper HTTPS detection behind Vercel's reverse proxy.
exportdefaultgetRequestListener(async(request,env)=>{constapp=awaitensureApp();constmethod=request.method.toUpperCase();constincoming=(envasVercelEnv)?.incoming;consturl=resolvePublicUrl(request.url,incoming);if(method!=='GET'&&method!=='HEAD'&&method!=='OPTIONS'&&incoming){constcontentType=incoming.headers?.['content-type'];constbody=extractBody(incoming,method,contentType);if(body!=null){returnawaitapp.fetch(newRequest(url,{ method,headers: request.headers, body }),);}}returnawaitapp.fetch(newRequest(url,{ method,headers: request.headers}),);});
  • 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

vercel 部署的studio,好像只接受 get api, post api 都超时

@vercel

vercelBot commented Apr 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
objectstack-playReadyReadyPreview, CommentApr 2, 2026 0:54am
specReadyReadyPreview, CommentApr 2, 2026 0:54am

Request Review

…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>
CopilotAI changed the title [WIP] Fix post API timeout issue in Vercel deploymentfix(studio): buffer POST body in Vercel outer→inner Hono delegationApr 2, 2026
CopilotAI requested a review from hotlongApril 2, 2026 11:48
@hotlong

Copy link
Copy Markdown
Contributor

@copilot 还是不行,参考这个,https://github.com/objectstack-ai/hotcrm/blob/main/api/%5B%5B...route%5D%5D.ts

…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>

CopilotAI commented Apr 2, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot 还是不行,参考这个,https://github.com/objectstack-ai/hotcrm/blob/main/api/%5B%5B...route%5D%5D.ts

Rewrote the Vercel handler to match the hotcrm pattern in a3f9662.

Replaced handle() + outer Hono app delegation with getRequestListener() from @hono/node-server which exposes env.incoming (the raw IncomingMessage). For POST/PUT/PATCH, the body is now extracted from Vercel's pre-buffered rawBody/body and a fresh standard Request is constructed — same extractBody() + resolvePublicUrl() pattern used in hotcrm.

CopilotAI changed the title fix(studio): buffer POST body in Vercel outer→inner Hono delegationfix(studio): use getRequestListener() with extractBody() for Vercel POST supportApr 2, 2026
@hotlong
hotlong marked this pull request as ready for review April 2, 2026 13:11
CopilotAI review requested due to automatic review settings April 2, 2026 13:11
@hotlong
hotlong merged commit 2296e19 into mainApr 2, 2026
3 checks passed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with getRequestListener() and an extractBody()-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.

FileDescription
apps/studio/server/index.tsImplements Vercel-safe request handling via getRequestListener(), extractBody(), and public URL protocol correction.
packages/adapters/hono/src/hono.test.tsAdds regression tests for request body forwarding through delegation patterns (including buffered forwarding).
apps/studio/CHANGELOG.mdRecords the Vercel POST/PUT/PATCH timeout fix and rationale.

Comment on lines +308 to +314
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);

CopilotAIApr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +335 to +342
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 }),
);

CopilotAIApr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +264 to +266
if (typeof incoming.body === 'string') return incoming.body;
if (contentType?.includes('application/json')) return JSON.stringify(incoming.body);
return String(incoming.body);

CopilotAIApr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if(typeofincoming.body==='string')returnincoming.body;
if(contentType?.includes('application/json'))returnJSON.stringify(incoming.body);
returnString(incoming.body);
constbody: unknown=incoming.body;
// Already-encoded string bodies can be forwarded as-is.
if(typeofbody==='string'){
returnbody;
}
// Preserve binary bodies.
if(typeofBuffer!=='undefined'&&Buffer.isBuffer(body)){
returnbody;
}
// Respect URLSearchParams bodies.
if(bodyinstanceofURLSearchParams){
returnbody;
}
// For form-urlencoded content, encode plain objects into a query string.
if(contentType?.includes('application/x-www-form-urlencoded')&&body&&typeofbody==='object'){
constparams=newURLSearchParams();
for(const[key,value]ofObject.entries(bodyasRecord<string,unknown>)){
if(Array.isArray(value)){
for(constvofvalue){
if(v!=null)params.append(key,String(v));
}
}elseif(value!=null){
params.append(key,String(value));
}
}
returnparams.toString();
}
// For JSON and other structured bodies, prefer JSON encoding over lossy String().
if(contentType?.includes('application/json')){
returnJSON.stringify(body);
}
if(typeofbody==='object'){
returnJSON.stringify(body);
}
// For primitive scalar values, String() is a faithful representation.
if(typeofbody==='number'||typeofbody==='boolean'||typeofbody==='bigint'){
returnString(body);
}

Copilot uses AI. Check for mistakes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@hotlong