fix(api): guard params await so static v1 routes don't 500 (#795)
Next.js 16 invokes a static route handler (no [segment]) with
{ params: undefined }. /api/v1/companies is the only authenticated static
route on the v1 surface, so awaiting params.params null-derefs and the catch
turns it into a 500 for every valid API key. Guard the await:
((await params?.params) ?? {}). Dynamic routes are unaffected. Fixes #781.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9ed0b9515a
commit
9278221616
@@ -83,6 +83,14 @@ function emptyParams() {
|
||||
return { params: Promise.resolve({}) }
|
||||
}
|
||||
|
||||
// What Next.js 16 ACTUALLY passes to a STATIC route's handler: `{ params:
|
||||
// undefined }` (app-route module: `params: context.params ? ... : undefined`).
|
||||
// `emptyParams()` above is NOT what the runtime hands a static route, so it
|
||||
// masked #781. Use this for the real static-route contract.
|
||||
function staticRouteContext() {
|
||||
return { params: undefined } as unknown as { params: Promise<Record<string, never>> }
|
||||
}
|
||||
|
||||
function companyParams(companyId: string) {
|
||||
return { params: Promise.resolve({ companyId }) }
|
||||
}
|
||||
@@ -246,6 +254,39 @@ describe('withApiV1 — company membership', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('withApiV1 — static (non-dynamic) route params', () => {
|
||||
// Regression for #781: GET /api/v1/companies is the only authenticated
|
||||
// static route. Next.js 16 hands it `{ params: undefined }`. The wrapper
|
||||
// must not null-deref on `params.params` after auth succeeds.
|
||||
it('does not 500 when Next passes { params: undefined } for a static route', async () => {
|
||||
mockValidate.mockResolvedValue({
|
||||
userId: 'user-1',
|
||||
companyId: undefined,
|
||||
scopes: ['companies:read'],
|
||||
mode: 'live',
|
||||
})
|
||||
|
||||
let observedCompanyId: string | undefined = 'sentinel'
|
||||
let handlerCalled = false
|
||||
const handler = withApiV1('companies.list', async (_req, ctx) => {
|
||||
handlerCalled = true
|
||||
observedCompanyId = ctx.companyId
|
||||
return ok({ ok: true }, { requestId: ctx.requestId })
|
||||
})
|
||||
|
||||
const res = await handler(
|
||||
makeRequest('https://x.test/api/v1/companies', {
|
||||
headers: { Authorization: 'Bearer gnubok_sk_x' },
|
||||
}),
|
||||
staticRouteContext(),
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(handlerCalled).toBe(true)
|
||||
expect(observedCompanyId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('withApiV1 — idempotency', () => {
|
||||
it('replays a cached response when the idempotency key matches', async () => {
|
||||
mockValidate.mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user