fix: unified catch-all dispatch pattern across all adapters to resolve Vercel 404s#931
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… meta query param passthrough Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
@copilot 每个adaptor都要单独这样修改吗?没有统一的方案吗? |
…ono, Express, Fastify, Next.js, Nuxt, SvelteKit, NestJS) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ispatch pattern Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Good point. Each adapter now only keeps explicit routes for framework-specific needs:
Everything else (meta, data, packages, analytics, automation, i18n, ui, openapi, custom endpoints) goes through the catch-all. This means new routes added to Net result: ~290 lines deleted across adapters. See commit |
There was a problem hiding this comment.
Pull request overview
This PR standardizes adapter routing by moving most API namespaces behind a single catch-all that forwards requests to HttpDispatcher.dispatch(), preventing missing-route 404s (notably in Vercel/serverless deployments) and reducing duplicated route registration logic across adapters.
Changes:
- Replaces per-namespace routing (meta/data/etc) with a unified catch-all delegating to
HttpDispatcher.dispatch()across adapters, keeping only framework-specific routes explicit (auth/storage/graphql/discovery). - Updates the Hono adapter test suite to validate catch-all dispatch forwarding (method/path/body/query).
- Adds adapter changelog entries describing the new routing approach.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/adapters/sveltekit/src/index.ts | Removes explicit meta/data handlers; adds catch-all dispatch() forwarding. |
| packages/adapters/sveltekit/CHANGELOG.md | Documents the catch-all dispatch change. |
| packages/adapters/nuxt/src/index.ts | Removes explicit meta/data; adds catch-all dispatch() handler and keeps explicit framework-specific routes. |
| packages/adapters/nuxt/CHANGELOG.md | Documents the catch-all dispatch change. |
| packages/adapters/nextjs/src/index.ts | Removes explicit meta/data; adds catch-all dispatch() forwarding for remaining namespaces. |
| packages/adapters/nextjs/CHANGELOG.md | Documents the catch-all dispatch change. |
| packages/adapters/nestjs/src/index.ts | Routes meta/data through dispatch() and adds controller-level catch-all. |
| packages/adapters/nestjs/CHANGELOG.md | Documents the catch-all dispatch change and meta/data routing update. |
| packages/adapters/hono/src/index.ts | Converts to explicit (auth/graphql/storage/discovery) + catch-all dispatch() pattern. |
| packages/adapters/hono/src/hono.test.ts | Rewrites tests to validate catch-all dispatch() behavior and query/body forwarding. |
| packages/adapters/hono/CHANGELOG.md | Documents the catch-all dispatch change and the Vercel 404 fix. |
| packages/adapters/fastify/src/index.ts | Removes explicit meta/data; adds catch-all dispatch() forwarding. |
| packages/adapters/fastify/CHANGELOG.md | Documents the catch-all dispatch change. |
| packages/adapters/express/src/index.ts | Removes explicit meta/data; adds catch-all dispatch() forwarding. |
| packages/adapters/express/CHANGELOG.md | Documents the catch-all dispatch change. |
| let path = req.params[0] || ''; | ||
| if (req.url.includes('/meta')) { | ||
| path = req.url.split('/meta')[1].split('?')[0]; | ||
| } | ||
|
|
||
| // Use injected body or fallback to req.body | ||
| const payload = body || req.body; | ||
|
|
||
| const result = await this.service.dispatcher.handleMetadata(path, { request: req }, req.method, payload); | ||
| const result = await this.service.dispatcher.dispatch(req.method, '/meta' + path, body || req.body, query, { request: req }); | ||
| return this.normalizeResponse(result, res); |
| // Catch-all: delegate remaining routes to dispatcher.dispatch() | ||
| // Handles packages, analytics, automation, i18n, ui, openapi, | ||
| // custom API endpoints, and any future routes. | ||
| @All('*') | ||
| async catchAll(@Req() req: any, @Res() res: any, @Body() body: any, @Query() query: any) { | ||
| try { | ||
| // Extract the sub-path after /api | ||
| const path = req.url.split('/api')[1]?.split('?')[0] || ''; | ||
| const method = req.method; | ||
| const payload = (method === 'POST' || method === 'PUT' || method === 'PATCH') ? body : undefined; | ||
| const result = await this.service.dispatcher.dispatch(method, path, payload, query, { request: req }); | ||
| return this.normalizeResponse(result, res); |
| ## 3.2.8 | ||
|
|
||
| ### Patch Changes | ||
|
|
||
| - fix: unified catch-all dispatch pattern — `createHonoApp()` now delegates all non-framework-specific routes to `HttpDispatcher.dispatch()`, automatically supporting packages, analytics, automation, i18n, ui, openapi, custom endpoints, and any future routes | ||
| - fix: resolves 404 errors for `/api/v1/meta` and `/api/v1/packages` after Vercel deployment | ||
| - Only auth (service check), storage (formData), GraphQL (raw result), and discovery (response wrapper) remain as explicit routes | ||
| - Added comprehensive tests for the catch-all dispatch pattern | ||
|
|
| fastify.all(`${prefix}/*`, async (request: FastifyRequest, reply: FastifyReply) => { | ||
| try { | ||
| const urlPath = request.url.split('?')[0]; | ||
| const subPath = urlPath.substring(`${prefix}/storage`.length); | ||
| const subPath = urlPath.substring(prefix.length); | ||
| const method = request.method; | ||
| const file = (request as any).file || (request.body as any)?.file; | ||
| const result = await dispatcher.handleStorage(subPath, method, file, { request: request.raw }); | ||
| const body = (method === 'POST' || method === 'PUT' || method === 'PATCH') ? request.body : undefined; | ||
| const result = await dispatcher.dispatch(method, subPath, body, request.query, { request: request.raw }); | ||
| return sendResult(result, reply); |
| const subPath = urlPath.substring(prefix.length); | ||
| const method = event.method; | ||
| const body = (method === 'POST' || method === 'PATCH') | ||
| const body = (method === 'POST' || method === 'PUT' || method === 'PATCH') | ||
| ? await readBody(event) | ||
| : {}; | ||
| : undefined; | ||
| const query = getQuery(event); | ||
| const result = await dispatcher.handleData(subPath, method, body, query, { request: event.node.req }); | ||
| return toResponse(event, result); | ||
| } catch (err: any) { | ||
| return errorJson(event, err.message || 'Internal Server Error', err.statusCode || 500); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| // --- Storage --- | ||
| router.use( | ||
| `${prefix}/storage/**`, | ||
| defineEventHandler(async (event) => { | ||
| try { | ||
| const urlPath = (event.path || event.node.req.url || '').split('?')[0]; | ||
| const subPath = urlPath.substring(`${prefix}/storage`.length); | ||
| const method = event.method; | ||
| const file = undefined; // File upload requires multipart parsing (e.g., formidable) | ||
| const result = await dispatcher.handleStorage(subPath, method, file, { request: event.node.req }); | ||
| const result = await dispatcher.dispatch(method, subPath, body, query, { request: event.node.req }); | ||
| return toResponse(event, result); |
| const subPath = path || ''; | ||
|
|
||
| let body: any = undefined; | ||
| if (method === 'POST' || method === 'PUT' || method === 'PATCH') { | ||
| body = await request.json().catch(() => ({})); | ||
| } | ||
|
|
||
| const queryParams: Record<string, any> = {}; | ||
| url.searchParams.forEach((val, key) => { queryParams[key] = val; }); | ||
|
|
||
| const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request }); | ||
| return toResponse(result); |
| // --- 4. Catch-all: delegate to dispatcher.dispatch() --- | ||
| // Handles meta, data, packages, analytics, automation, i18n, ui, | ||
| // openapi, custom API endpoints, and any future routes. | ||
| const path = '/' + segments.join('/'); | ||
|
|
||
| let body: any = undefined; | ||
| if (method === 'POST' || method === 'PUT' || method === 'PATCH') { | ||
| body = await req.json().catch(() => ({})); | ||
| } | ||
|
|
||
| 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 }); | ||
| return toResponse(result); |
All 7 adapters (Hono, Express, Fastify, Next.js, Nuxt, SvelteKit, NestJS) manually registered individual routes for each API namespace — and all were missing
/packages,/analytics,/automation,/i18n, and/ui. This caused 404s on Vercel (and any other deployment) for those routes.Rather than adding the missing routes one by one to each adapter, this PR introduces a unified catch-all pattern: each adapter now delegates to
HttpDispatcher.dispatch()for all non-framework-specific routes. New routes added todispatch()automatically work in every adapter with zero adapter-side code changes.Unified catch-all architecture
Each adapter keeps only routes that require framework-specific handling:
getServiceAsync('auth'))HttpDispatcherResult){ data: ... })Everything else (meta, data, packages, analytics, automation, i18n, ui, openapi, custom API endpoints, and any future routes) goes through a single catch-all that delegates to
dispatcher.dispatch().Adapters updated
@objectstack/hono—createHonoApp()@objectstack/express—createExpressRouter()@objectstack/fastify—objectStackPlugin()@objectstack/nextjs—createRouteHandler()@objectstack/nuxt—createH3Router()@objectstack/sveltekit—createRequestHandler()@objectstack/nestjs—ObjectStackControllerTests
dispatch()Impact
/api/v1/meta,/api/v1/packages, and all other previously unregistered routesHttpDispatcher.dispatch()are automatically available in all adaptersOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.