Skip to content

Fix discovery API path missing /api/v1 prefix on Vercel deployment#1064

Merged
hotlong merged 2 commits intomainfrom
claude/fix-discovery-api-path
Apr 2, 2026
Merged

Fix discovery API path missing /api/v1 prefix on Vercel deployment#1064
hotlong merged 2 commits intomainfrom
claude/fix-discovery-api-path

Conversation

@Claude
Copy link
Copy Markdown
Contributor

@Claude Claude AI commented Apr 2, 2026

Problem

After deploying to Vercel, the discovery endpoint (GET /api/v1/discovery) returns API paths without the /api/v1 prefix. For example, routes appear as /auth, /data instead of /api/v1/auth, /api/v1/data, causing the API console to break. Locally this issue was hidden because the MSW-based dev environment only registers minimal services, so the wrong routes were never actually used.

Root Cause

All framework adapters registered an explicit discovery handler only at GET prefix (e.g., GET /api/v1), but not at GET prefix/discovery (e.g., GET /api/v1/discovery). When the frontend fetched /api/v1/discovery, the catch-all handler stripped the prefix and called dispatcher.dispatch('GET', '/discovery', ...). Inside dispatch(), the discovery branch called getDiscoveryInfo('') with an empty prefix, returning routes like /data and /auth instead of /api/v1/data and /api/v1/auth.

On Vercel, all services (auth, ai, security, audit, feed, etc.) are fully registered, so the broken routes from discovery were actually consumed by the API console, causing it to fail. Locally with MSW, only minimal services are registered and the console falls back to hardcoded defaults, masking the bug.

Changes

packages/runtime/src/http-dispatcher.ts

  • Added optional prefix parameter (6th arg, backward-compatible) to dispatch() method
  • dispatch() now passes prefix to getDiscoveryInfo() when handling the /discovery route, instead of using an empty string

All adapters — Added explicit GET ${prefix}/discovery route

  • packages/adapters/hono/src/index.ts — Added explicit GET ${prefix}/discovery route calling getDiscoveryInfo(prefix) (primary Vercel fix)
  • packages/adapters/fastify/src/index.ts — Same fix
  • packages/adapters/nuxt/src/index.ts — Same fix
  • packages/adapters/sveltekit/src/index.ts — Handles discovery segment before catch-all
  • packages/adapters/nextjs/src/index.ts — Handles discovery segment before catch-all
  • packages/adapters/express/src/index.ts — Passes prefix to dispatch() (already had explicit /discovery route)

packages/plugins/plugin-msw/src/msw-plugin.ts

  • Added explicit GET baseUrl/discovery MSW handler with correct prefix
  • Passes baseUrl as prefix to dispatcher.dispatch() in the catch-all

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

  • Updated all dispatch() call assertions to include the new prefix 6th argument
  • Added tests for GET /api/discovery and GET /api/v1/discovery endpoints via the Vercel delegation pattern

Example

// Before: GET /api/v1/discovery falls through to dispatch('/discovery')
// which calls getDiscoveryInfo('') — routes returned without prefix
// { routes: { data: '/data', auth: '/auth' } }  ← wrong!

// After: GET /api/v1/discovery is handled by explicit route
// which calls getDiscoveryInfo('/api/v1') — routes include correct prefix
// { routes: { data: '/api/v1/data', auth: '/api/v1/auth' } }  ← correct!

This ensures discovery responses always include the correct API prefix across all deployment environments (Vercel, local, etc.).

- Updated ObjectQL protocol getDiscovery() to accept optional prefix parameter
- Modified SERVICE_CONFIG to use relative paths instead of hardcoded /api/v1 prefix
- Updated RestServer to pass basePath to protocol.getDiscovery()
- Updated GetDiscoveryRequestSchema to include optional prefix field
- This fixes the issue where discovery returned hardcoded /api/v1 paths on Vercel

Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/be36951d-a538-45c9-a17f-126fdc152c53

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@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 11:07am
spec Ready Ready Preview, Comment Apr 2, 2026 11:07am

Request Review

@github-actions github-actions bot added the size/s label Apr 2, 2026
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 2, 2026

@copilot 你开发的不对,现在问题都关键是本地运行的时候是正常的,但是部署之后返回的API路由不对,导致API console坏掉了,查一下这两个部署有什么区别

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

Copilot AI commented Apr 2, 2026

@copilot 你开发的不对,现在问题都关键是本地运行的时候是正常的,但是部署之后返回的API路由不对,导致API console坏掉了,查一下这两个部署有什么区...

找到根本原因了。问题不在 protocol.ts,而是在 dispatcher 层:

本地(MSW模式)vs Vercel 的关键区别:

  • 本地:MSW 拦截所有请求,只注册了少量服务(无 auth、ai 等),所以 discovery 返回的错误路由不会被实际使用
  • Vercel:注册了完整的服务(auth、ai、security 等),discovery 返回错误路由导致 API Console 全部崩溃

根本原因:所有 adapter 在处理 GET /api/v1/discovery 时,catch-all 会把 prefix 剥离后调用 dispatcher.dispatch('GET', '/discovery', ...),而 dispatch() 内部调用 getDiscoveryInfo('')(空 prefix),返回的路由没有 /api/v1 前缀(如 /data/auth 而非 /api/v1/data/api/v1/auth)。

修复(commit d84fb63):

  1. 在所有 adapter(hono、fastify、nuxt、sveltekit、nextjs、express)中添加显式的 GET ${prefix}/discovery 路由,调用正确的 getDiscoveryInfo(prefix)
  2. MSW plugin 添加显式的 baseUrl/discovery handler
  3. http-dispatcher.dispatch() 新增可选 prefix 参数,确保通过 catch-all 进来的 discovery 请求也能使用正确的前缀
  4. 回退了之前错误方向的 protocol.ts 修改

Copilot AI changed the title Fix discovery API path generation to respect prefix parameter Fix discovery API path missing /api/v1 prefix on Vercel deployment Apr 2, 2026
Copilot AI requested a review from hotlong April 2, 2026 11:04
@hotlong hotlong marked this pull request as ready for review April 2, 2026 11:06
Copilot AI review requested due to automatic review settings April 2, 2026 11:06
@hotlong hotlong merged commit 12bade7 into main Apr 2, 2026
5 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 discovery responses returning routes without the deployment prefix (e.g. /data instead of /api/v1/data) by ensuring the dispatcher and adapters preserve/pass the API prefix when serving GET .../discovery, particularly in Vercel deployments where the prefix-stripping catch-all path was previously invoked.

Changes:

  • Extended HttpDispatcher.dispatch() with an optional prefix parameter and used it for discovery generation.
  • Added explicit GET ${prefix}/discovery handling (or equivalent segment handling) across adapters and ensured adapter catch-alls pass prefix into dispatch().
  • Updated Hono adapter tests to cover /discovery and the Vercel delegation pattern, and updated dispatch-argument assertions there.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/runtime/src/http-dispatcher.ts Adds optional prefix to dispatch() and forwards it into discovery generation.
packages/plugins/plugin-msw/src/msw-plugin.ts Adds explicit MSW handlers for ${baseUrl} and ${baseUrl}/discovery and passes baseUrl into dispatch().
packages/adapters/sveltekit/src/index.ts Handles GET .../discovery explicitly and passes prefix into dispatch().
packages/adapters/nuxt/src/index.ts Registers GET ${prefix}/discovery and passes prefix into dispatch().
packages/adapters/nextjs/src/index.ts Handles GET .../discovery segment explicitly and passes prefix into dispatch().
packages/adapters/hono/src/index.ts Registers GET ${prefix}/discovery and passes prefix into dispatch().
packages/adapters/hono/src/hono.test.ts Adds /discovery coverage and updates dispatch() call assertions for the new prefix arg.
packages/adapters/fastify/src/index.ts Registers GET ${prefix}/discovery and passes prefix into dispatch().
packages/adapters/express/src/index.ts Passes prefix into dispatch() for the catch-all.

Comment on lines 176 to 180
const subPath = urlPath.substring(prefix.length);
const method = request.method;
const body = (method === 'POST' || method === 'PUT' || method === 'PATCH') ? request.body : undefined;
const result = await dispatcher.dispatch(method, subPath, body, request.query, { request: request.raw });
const result = await dispatcher.dispatch(method, subPath, body, request.query, { request: request.raw }, prefix);
return sendResult(result, reply);
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 adapter now passes a 6th prefix argument to dispatcher.dispatch(...), but the existing Fastify adapter unit tests still assert the old 5-argument call signature (see packages/adapters/fastify/src/fastify.test.ts). This will cause the test suite to fail unless the expectations are updated to include the prefix (e.g. '/api' / custom prefix) and ideally add coverage for the new GET ${prefix}/discovery route.

Copilot uses AI. Check for mistakes.
Comment on lines 216 to 221
const body = (method === 'POST' || method === 'PUT' || method === 'PATCH')
? await readBody(event)
: undefined;
const query = getQuery(event);
const result = await dispatcher.dispatch(method, subPath, body, query, { request: event.node.req });
const result = await dispatcher.dispatch(method, subPath, body, query, { request: event.node.req }, prefix);
return toResponse(event, result);
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 Nuxt adapter now passes a 6th prefix argument to dispatcher.dispatch(...), but packages/adapters/nuxt/src/nuxt.test.ts still asserts the old 5-argument call signature. Update those expectations to include the prefix and add a test for GET ${prefix}/discovery to prevent regressions of this specific bug.

Copilot uses AI. Check for mistakes.
Comment on lines 183 to 187
const queryParams: Record<string, any> = {};
url.searchParams.forEach((val, key) => { queryParams[key] = val; });

const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request });
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request }, prefix);
return toResponse(result);
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 SvelteKit adapter now passes a 6th prefix argument to dispatcher.dispatch(...), but packages/adapters/sveltekit/src/sveltekit.test.ts still asserts the old 5-argument signature. Update the test expectations to include the prefix, and consider adding a dedicated test for GET ${prefix}/discovery (added above) since that’s the primary regression being fixed.

Copilot uses AI. Check for mistakes.
Comment on lines 138 to 143
const url = new URL(req.url);
const queryParams: Record<string, any> = {};
url.searchParams.forEach((val, key) => queryParams[key] = val);

const result = await dispatcher.dispatch(method, path, body, queryParams, { request: rawRequest });
const result = await dispatcher.dispatch(method, path, body, queryParams, { request: rawRequest }, options.prefix || '/api');
return toResponse(result);
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 Next.js adapter now passes a 6th prefix argument to dispatcher.dispatch(...), but packages/adapters/nextjs/src/nextjs.test.ts still asserts the old 5-argument call signature. Update those expectations to include the prefix, and add coverage for the new GET .../discovery branch (segments === ['discovery']) to ensure the Vercel case stays fixed.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +245
http.get(`*${baseUrl}`, async () => {
if (this.dispatcher) {
return HttpResponse.json({ data: await this.dispatcher.getDiscoveryInfo(baseUrl) });
}
return HttpResponse.json({ data: { version: 'v1', url: baseUrl } });
}),
http.get(`*${baseUrl}/discovery`, async () => {
if (this.dispatcher) {
return HttpResponse.json({ data: await this.dispatcher.getDiscoveryInfo(baseUrl) });
}
return HttpResponse.json({ data: { version: 'v1', url: baseUrl } });
})
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 fallback discovery payload returned when this.dispatcher is unavailable ({ version: 'v1', url: baseUrl }) does not match the shape returned by HttpDispatcher.getDiscoveryInfo() or even the more complete fallback used by the /.well-known/objectstack handler above. This can break clients that rely on fields like routes/endpoints/features even in MSW fallback mode. Consider reusing the same fallback object as the well-known handler (or a minimal stub that still includes routes and features keys with sensible defaults) for consistency across discovery endpoints.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants