Skip to content

Commit d84fb63

Browse files
Copilothotlong
andauthored
Fix discovery endpoint returning routes without /api/v1 prefix on Vercel
Root cause: all adapters registered discovery only at GET prefix (root), not at GET prefix/discovery. When frontend fetched /api/v1/discovery, the catch-all stripped the prefix and called dispatch('/discovery') which called getDiscoveryInfo('') with empty prefix, returning /data instead of /api/v1/data. Locally this was hidden because MSW mode doesn't register real services, so the wrong routes were never used. On Vercel, all services are registered, breaking the API console. Fixes: - http-dispatcher: add optional prefix param to dispatch() so discovery handler uses the correct prefix instead of empty string - hono adapter: add explicit GET prefix/discovery route (Vercel fix) - fastify, nuxt adapters: add explicit GET prefix/discovery route - sveltekit, nextjs adapters: handle 'discovery' segment before catch-all - express adapter: pass prefix to dispatch() (already had explicit route) - MSW plugin: add explicit baseUrl/discovery handler + pass baseUrl to dispatch() - Revert wrong protocol.ts/rest-server.ts/spec changes from previous commit - Update hono test assertions for new 6th prefix parameter Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/ae9fa3e0-4bf7-45e8-b4ee-4816ad2f6917 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 060bca4 commit d84fb63

12 files changed

Lines changed: 131 additions & 53 deletions

File tree

packages/adapters/express/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export function createExpressRouter(options: ExpressAdapterOptions): Router {
167167
const subPath = '/' + (req.params as any).path;
168168
const method = req.method;
169169
const body = (method === 'POST' || method === 'PUT' || method === 'PATCH') ? req.body : undefined;
170-
const result = await dispatcher.dispatch(method, subPath, body, req.query, { request: req, response: res });
170+
const result = await dispatcher.dispatch(method, subPath, body, req.query, { request: req, response: res }, prefix);
171171
return sendResult(result, res);
172172
} catch (err: any) {
173173
return errorResponse(err, res);

packages/adapters/fastify/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export async function objectStackPlugin(fastify: FastifyInstance, options: Fasti
8080
return reply.send({ data: await dispatcher.getDiscoveryInfo(prefix) });
8181
});
8282

83+
fastify.get(`${prefix}/discovery`, async (_request: FastifyRequest, reply: FastifyReply) => {
84+
return reply.send({ data: await dispatcher.getDiscoveryInfo(prefix) });
85+
});
86+
8387
// --- .well-known ---
8488
fastify.get('/.well-known/objectstack', async (_request: FastifyRequest, reply: FastifyReply) => {
8589
return reply.redirect(prefix);
@@ -172,7 +176,7 @@ export async function objectStackPlugin(fastify: FastifyInstance, options: Fasti
172176
const subPath = urlPath.substring(prefix.length);
173177
const method = request.method;
174178
const body = (method === 'POST' || method === 'PUT' || method === 'PATCH') ? request.body : undefined;
175-
const result = await dispatcher.dispatch(method, subPath, body, request.query, { request: request.raw });
179+
const result = await dispatcher.dispatch(method, subPath, body, request.query, { request: request.raw }, prefix);
176180
return sendResult(result, reply);
177181
} catch (err: any) {
178182
return errorResponse(err, reply);

packages/adapters/hono/src/hono.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ describe('createHonoApp', () => {
9797
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
9898
});
9999

100+
it('GET /api/discovery returns discovery info with correct prefix', async () => {
101+
const res = await app.request('/api/discovery');
102+
expect(res.status).toBe(200);
103+
const json = await res.json();
104+
expect(json.data).toBeDefined();
105+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
106+
});
107+
100108
it('uses custom prefix for discovery', async () => {
101109
const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
102110
const res = await customApp.request('/v2');
@@ -105,6 +113,15 @@ describe('createHonoApp', () => {
105113
expect(json.data).toBeDefined();
106114
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
107115
});
116+
117+
it('uses custom prefix for /discovery route', async () => {
118+
const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
119+
const res = await customApp.request('/v2/discovery');
120+
expect(res.status).toBe(200);
121+
const json = await res.json();
122+
expect(json.data).toBeDefined();
123+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
124+
});
108125
});
109126

110127
describe('.well-known Endpoint', () => {
@@ -277,6 +294,7 @@ describe('createHonoApp', () => {
277294
undefined,
278295
expect.any(Object),
279296
expect.objectContaining({ request: expect.anything() }),
297+
'/api',
280298
);
281299
});
282300

@@ -294,6 +312,7 @@ describe('createHonoApp', () => {
294312
body,
295313
expect.any(Object),
296314
expect.objectContaining({ request: expect.anything() }),
315+
'/api',
297316
);
298317
});
299318

@@ -306,6 +325,7 @@ describe('createHonoApp', () => {
306325
undefined,
307326
expect.any(Object),
308327
expect.objectContaining({ request: expect.anything() }),
328+
'/api',
309329
);
310330
});
311331

@@ -318,6 +338,7 @@ describe('createHonoApp', () => {
318338
undefined,
319339
expect.objectContaining({ package: 'com.acme.crm' }),
320340
expect.objectContaining({ request: expect.anything() }),
341+
'/api',
321342
);
322343
});
323344

@@ -330,6 +351,7 @@ describe('createHonoApp', () => {
330351
undefined,
331352
expect.any(Object),
332353
expect.objectContaining({ request: expect.anything() }),
354+
'/api',
333355
);
334356
});
335357

@@ -347,6 +369,7 @@ describe('createHonoApp', () => {
347369
body,
348370
expect.any(Object),
349371
expect.objectContaining({ request: expect.anything() }),
372+
'/api',
350373
);
351374
});
352375

@@ -364,6 +387,7 @@ describe('createHonoApp', () => {
364387
body,
365388
expect.any(Object),
366389
expect.objectContaining({ request: expect.anything() }),
390+
'/api',
367391
);
368392
});
369393

@@ -384,6 +408,7 @@ describe('createHonoApp', () => {
384408
undefined,
385409
expect.any(Object),
386410
expect.objectContaining({ request: expect.anything() }),
411+
'/api',
387412
);
388413
});
389414

@@ -396,6 +421,7 @@ describe('createHonoApp', () => {
396421
undefined,
397422
expect.any(Object),
398423
expect.objectContaining({ request: expect.anything() }),
424+
'/api',
399425
);
400426
});
401427

@@ -413,6 +439,7 @@ describe('createHonoApp', () => {
413439
body,
414440
expect.any(Object),
415441
expect.objectContaining({ request: expect.anything() }),
442+
'/api',
416443
);
417444
});
418445

@@ -425,6 +452,7 @@ describe('createHonoApp', () => {
425452
undefined,
426453
expect.objectContaining({ status: 'active' }),
427454
expect.objectContaining({ request: expect.anything() }),
455+
'/api',
428456
);
429457
});
430458

@@ -446,6 +474,7 @@ describe('createHonoApp', () => {
446474
undefined,
447475
expect.any(Object),
448476
expect.objectContaining({ request: expect.anything() }),
477+
'/api',
449478
);
450479
});
451480

@@ -458,6 +487,7 @@ describe('createHonoApp', () => {
458487
undefined,
459488
expect.any(Object),
460489
expect.objectContaining({ request: expect.anything() }),
490+
'/api',
461491
);
462492
});
463493

@@ -470,6 +500,7 @@ describe('createHonoApp', () => {
470500
undefined,
471501
expect.any(Object),
472502
expect.objectContaining({ request: expect.anything() }),
503+
'/api',
473504
);
474505
});
475506

@@ -482,6 +513,7 @@ describe('createHonoApp', () => {
482513
undefined,
483514
expect.any(Object),
484515
expect.objectContaining({ request: expect.anything() }),
516+
'/api',
485517
);
486518
});
487519
});
@@ -571,6 +603,7 @@ describe('createHonoApp', () => {
571603
undefined,
572604
expect.any(Object),
573605
expect.objectContaining({ request: expect.anything() }),
606+
'/api/v1',
574607
);
575608
});
576609

@@ -585,6 +618,7 @@ describe('createHonoApp', () => {
585618
undefined,
586619
expect.any(Object),
587620
expect.objectContaining({ request: expect.anything() }),
621+
'/api/v1',
588622
);
589623
});
590624

@@ -598,6 +632,16 @@ describe('createHonoApp', () => {
598632
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
599633
});
600634

635+
it('routes /api/v1/discovery through outer→inner delegation with correct prefix', async () => {
636+
const outerApp = createVercelApp();
637+
638+
const res = await outerApp.request('/api/v1/discovery');
639+
expect(res.status).toBe(200);
640+
const json = await res.json();
641+
expect(json.data).toBeDefined();
642+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
643+
});
644+
601645
it('routes /api/v1/data/account through outer→inner delegation', async () => {
602646
const outerApp = createVercelApp();
603647

@@ -609,6 +653,7 @@ describe('createHonoApp', () => {
609653
undefined,
610654
expect.any(Object),
611655
expect.objectContaining({ request: expect.anything() }),
656+
'/api/v1',
612657
);
613658
});
614659

packages/adapters/hono/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
112112
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
113113
});
114114

115+
app.get(`${prefix}/discovery`, async (c) => {
116+
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
117+
});
118+
115119
// --- .well-known ---
116120
app.get('/.well-known/objectstack', (c) => {
117121
return c.redirect(prefix);
@@ -202,7 +206,7 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
202206
const url = new URL(c.req.url);
203207
url.searchParams.forEach((val, key) => { queryParams[key] = val; });
204208

205-
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
209+
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);
206210
return toResponse(c, result);
207211
} catch (err: any) {
208212
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);

packages/adapters/nextjs/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export function createRouteHandler(options: NextAdapterOptions) {
6666
return NextResponse.json({ data: await dispatcher.getDiscoveryInfo(options.prefix || '/api') });
6767
}
6868

69+
if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
70+
return NextResponse.json({ data: await dispatcher.getDiscoveryInfo(options.prefix || '/api') });
71+
}
72+
6973
try {
7074
const rawRequest = req;
7175

@@ -135,7 +139,7 @@ export function createRouteHandler(options: NextAdapterOptions) {
135139
const queryParams: Record<string, any> = {};
136140
url.searchParams.forEach((val, key) => queryParams[key] = val);
137141

138-
const result = await dispatcher.dispatch(method, path, body, queryParams, { request: rawRequest });
142+
const result = await dispatcher.dispatch(method, path, body, queryParams, { request: rawRequest }, options.prefix || '/api');
139143
return toResponse(result);
140144

141145
} catch (err: any) {

packages/adapters/nuxt/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ export function createH3Router(options: NuxtAdapterOptions): Router {
9595
}),
9696
);
9797

98+
router.get(
99+
`${prefix}/discovery`,
100+
defineEventHandler(async () => {
101+
return { data: await dispatcher.getDiscoveryInfo(prefix) };
102+
}),
103+
);
104+
98105
// --- .well-known ---
99106
router.get(
100107
'/.well-known/objectstack',
@@ -210,7 +217,7 @@ export function createH3Router(options: NuxtAdapterOptions): Router {
210217
? await readBody(event)
211218
: undefined;
212219
const query = getQuery(event);
213-
const result = await dispatcher.dispatch(method, subPath, body, query, { request: event.node.req });
220+
const result = await dispatcher.dispatch(method, subPath, body, query, { request: event.node.req }, prefix);
214221
return toResponse(event, result);
215222
} catch (err: any) {
216223
return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500);

packages/adapters/sveltekit/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ export function createRequestHandler(options: SvelteKitAdapterOptions) {
106106
});
107107
}
108108

109+
if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
110+
return new Response(JSON.stringify({ data: await dispatcher.getDiscoveryInfo(prefix) }), {
111+
status: 200,
112+
headers: { 'Content-Type': 'application/json' },
113+
});
114+
}
115+
109116
try {
110117
// --- Auth (needs auth service integration) ---
111118
if (segments[0] === 'auth') {
@@ -176,7 +183,7 @@ export function createRequestHandler(options: SvelteKitAdapterOptions) {
176183
const queryParams: Record<string, any> = {};
177184
url.searchParams.forEach((val, key) => { queryParams[key] = val; });
178185

179-
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request });
186+
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request }, prefix);
180187
return toResponse(result);
181188
} catch (err: any) {
182189
return errorJson(err.message || 'Internal Server Error', err.statusCode || 500);

0 commit comments

Comments
 (0)