Skip to content

Commit 999ea4f

Browse files
authored
Merge pull request #1001 from objectstack-ai/copilot/fix-api-endpoint-responses
fix(studio): replace custom getRequestListener with handle() from @hono/node-server/vercel
2 parents cc52e5c + 1624851 commit 999ea4f

File tree

2 files changed

+48
-83
lines changed

2 files changed

+48
-83
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/studio": patch
3+
---
4+
5+
Fix Vercel deployment API endpoints returning HTML instead of JSON.
6+
7+
Replace the custom `getRequestListener` export in `server/index.ts` with the
8+
standard `handle()` adapter from `@hono/node-server/vercel` and the
9+
outer→inner Hono delegation pattern (`inner.fetch(c.req.raw)`).
10+
11+
- The `handle()` adapter correctly wraps the Hono app with the
12+
`(IncomingMessage, ServerResponse) => Promise<void>` signature that
13+
Vercel's Node.js runtime expects for serverless functions in `api/`.
14+
- `@hono/node-server/vercel`'s `getRequestListener()` already handles
15+
Vercel's pre-buffered `rawBody` natively, removing the need for the
16+
custom body-extraction helper.
17+
- The outer→inner delegation pattern matches the documented ObjectStack
18+
Vercel deployment guide and the `@objectstack/hono` adapter test suite.

apps/studio/server/index.ts

Lines changed: 30 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
* legacy `(IncomingMessage, ServerResponse)` signature — NOT the Web standard
1111
* `(Request) → Response` format.
1212
*
13-
* We use `getRequestListener()` from `@hono/node-server` which properly
14-
* converts `IncomingMessage → Request`, calls our fetch callback, then writes
15-
* the `Response` back to `ServerResponse`.
13+
* We use `handle()` from `@hono/node-server/vercel` which is the standard
14+
* Vercel adapter for Hono. It internally uses `getRequestListener()` to
15+
* convert `IncomingMessage → Request` (including Vercel's pre-buffered
16+
* `rawBody`) and writes the `Response` back to `ServerResponse`.
1617
*
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).
18+
* The outer Hono app delegates all requests to the inner ObjectStack Hono
19+
* app via `inner.fetch(c.req.raw)`, matching the pattern documented in
20+
* the ObjectStack deployment guide and validated by the hono adapter tests.
2221
*
2322
* All kernel/service initialisation is co-located here so there are no
2423
* extensionless relative module imports — which would break Node's ESM
@@ -34,8 +33,8 @@ import { SecurityPlugin } from '@objectstack/plugin-security';
3433
import { AuditPlugin } from '@objectstack/plugin-audit';
3534
import { FeedServicePlugin } from '@objectstack/service-feed';
3635
import { MetadataPlugin } from '@objectstack/metadata';
37-
import { getRequestListener } from '@hono/node-server';
38-
import type { Hono } from 'hono';
36+
import { handle } from '@hono/node-server/vercel';
37+
import { Hono } from 'hono';
3938
import { createBrokerShim } from '../src/lib/create-broker-shim.js';
4039
import studioConfig from '../objectstack.config.js';
4140

@@ -198,93 +197,41 @@ async function ensureApp(): Promise<Hono> {
198197
return _app;
199198
}
200199

201-
// ---------------------------------------------------------------------------
202-
// Body extraction helpers
203-
// ---------------------------------------------------------------------------
204-
205-
/**
206-
* Extract the request body from the Vercel IncomingMessage.
207-
*
208-
* Vercel's Node.js runtime pre-buffers the full request body and attaches it
209-
* to the IncomingMessage as `rawBody` (Buffer) and/or `body` (parsed).
210-
* Reading from these properties is synchronous and avoids the fragile
211-
* IncomingMessage → ReadableStream conversion that can hang when the
212-
* underlying Node stream has already been consumed.
213-
*
214-
* Returns `null` for GET/HEAD/OPTIONS or when no body is available.
215-
*/
216-
function extractBody(incoming: any, method: string, contentType: string | undefined): BodyInit | null {
217-
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
218-
return null;
219-
}
220-
221-
// 1. rawBody (Buffer or string) — most reliable, set by Vercel runtime
222-
if (incoming?.rawBody != null) {
223-
if (typeof incoming.rawBody === 'string') return incoming.rawBody;
224-
if (typeof incoming.rawBody.toString === 'function') return incoming.rawBody;
225-
return String(incoming.rawBody);
226-
}
227-
228-
// 2. body (parsed by Vercel) — re-serialize based on content-type
229-
if (incoming?.body != null) {
230-
if (typeof incoming.body === 'string') return incoming.body;
231-
if (contentType?.includes('application/json')) return JSON.stringify(incoming.body);
232-
return String(incoming.body);
233-
}
234-
235-
return null;
236-
}
237-
238200
// ---------------------------------------------------------------------------
239201
// Vercel handler
240202
// ---------------------------------------------------------------------------
241203

242204
/**
243-
* `getRequestListener` from `@hono/node-server` converts
244-
* `IncomingMessage → Request`, calls our fetch callback, then writes the
245-
* `Response` back to `ServerResponse` (including `res.end()`).
205+
* Outer Hono app — delegates all requests to the inner ObjectStack app.
206+
*
207+
* `handle()` from `@hono/node-server/vercel` wraps any Hono app and returns
208+
* the `(IncomingMessage, ServerResponse) => Promise<void>` signature that
209+
* Vercel's Node.js runtime expects for serverless functions. Internally it
210+
* uses `getRequestListener()`, which already handles Vercel's pre-buffered
211+
* `rawBody` (Buffer) on the IncomingMessage for POST/PUT/PATCH requests.
246212
*
247-
* For requests with a body, we extract it from the IncomingMessage directly
248-
* (bypassing the Node stream → ReadableStream conversion) and create a new
249-
* Request that the inner Hono app can safely `.json()`.
213+
* The outer→inner delegation pattern (`inner.fetch(c.req.raw)`) is the
214+
* standard ObjectStack Vercel deployment pattern documented in the deployment
215+
* guide and covered by the @objectstack/hono adapter test suite.
250216
*/
251-
export default getRequestListener(async (request, env) => {
252-
const method = request.method;
253-
const url = request.url;
217+
const app = new Hono();
254218

255-
console.log(`[Vercel] ${method} ${url}`);
219+
app.all('*', async (c) => {
220+
console.log(`[Vercel] ${c.req.method} ${c.req.url}`);
256221

257222
try {
258-
const app = await ensureApp();
259-
const incoming = (env as any)?.incoming;
260-
261-
// For body methods, extract body from IncomingMessage and build a clean Request
262-
if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && incoming) {
263-
const contentType = incoming.headers?.['content-type'];
264-
const body = extractBody(incoming, method, contentType);
265-
266-
if (body != null) {
267-
console.log(`[Vercel] Body extracted from IncomingMessage (${typeof body === 'string' ? body.length + ' chars' : 'Buffer'})`);
268-
const newReq = new Request(url, {
269-
method,
270-
headers: request.headers,
271-
body,
272-
});
273-
return await app.fetch(newReq);
274-
}
275-
276-
console.log('[Vercel] No rawBody/body on IncomingMessage — using proxy request');
277-
}
278-
279-
return await app.fetch(request);
223+
const inner = await ensureApp();
224+
return await inner.fetch(c.req.raw);
280225
} catch (err: any) {
281226
console.error('[Vercel] Handler error:', err?.message || err);
282-
return new Response(
283-
JSON.stringify({
227+
return c.json(
228+
{
284229
success: false,
285230
error: { message: err?.message || 'Internal Server Error', code: 500 },
286-
}),
287-
{ status: 500, headers: { 'Content-Type': 'application/json' } },
231+
},
232+
500,
288233
);
289234
}
290235
});
236+
237+
export default handle(app);

0 commit comments

Comments
 (0)