Skip to content

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

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

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

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

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.
export default getRequestListener(async (request, env) => {
    const app = await ensureApp();
    const method = request.method.toUpperCase();
    const incoming = (env as VercelEnv)?.incoming;
    const url = resolvePublicUrl(request.url, incoming);

    if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && incoming) {
        const contentType = incoming.headers?.['content-type'];
        const body = extractBody(incoming, method, contentType);
        if (body != null) {
            return await app.fetch(
                new Request(url, { method, headers: request.headers, body }),
            );
        }
    }

    return await app.fetch(
        new Request(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
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

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

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Apr 2, 2026 0:54am
spec Ready Ready Preview, Comment Apr 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>
Copilot AI changed the title [WIP] Fix post API timeout issue in Vercel deployment fix(studio): buffer POST body in Vercel outer→inner Hono delegation Apr 2, 2026
Copilot AI requested a review from hotlong April 2, 2026 11:48
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 2, 2026

…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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@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.

Copilot AI changed the title fix(studio): buffer POST body in Vercel outer→inner Hono delegation fix(studio): use getRequestListener() with extractBody() for Vercel POST support Apr 2, 2026
@hotlong hotlong marked this pull request as ready for review April 2, 2026 13:11
Copilot AI review requested due to automatic review settings April 2, 2026 13:11
@hotlong hotlong merged commit 2296e19 into main Apr 2, 2026
3 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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/vercel handle() + 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.

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.

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);
Copy link

Copilot AI Apr 2, 2026

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 }),
);
Copy link

Copilot AI Apr 2, 2026

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);
Copy link

Copilot AI Apr 2, 2026

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 (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);
}

Copilot uses AI. Check for mistakes.
Sign up for free to 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