Throw when generateStaticParams returns invalid values (#95968)

> [!TIP]
> This PR is best reviewed commit by commit.

### Why?

`generateStaticParams` must return an array of objects, but Next.js does
not currently provide a clear error when it returns something else.

### How?

- Throw when `generateStaticParams` does not return an array.
- Throw when an item in the returned array is not an object.
- Keep allowing `{}`. The follow-up rejects it with `output: 'export'`
when it leaves a dynamic route parameter missing.

Follow-up: #95969

Supersedes #95388.

<!-- NEXT_JS_LLM -->

Co-authored-by: SukkaW <isukkaw@gmail.com>

---------

Co-authored-by: Joseph <joseph.chamochumbi@vercel.com>
This commit is contained in:
Jiwon Choi
2026-07-23 23:25:55 +02:00
committed by GitHub
parent 45eb5ae0bc
commit 81643544fc
6 changed files with 346 additions and 5 deletions
+28
View File
@@ -0,0 +1,28 @@
---
title: 'Invalid `generateStaticParams` Value'
---
## Why This Error Occurred
Next.js expects `generateStaticParams` to return an array containing one params object for each route to generate.
This error occurs when:
- `generateStaticParams` returns a value that is not an array, or
- an item in the returned array is not a plain object. This includes `null`, arrays, primitive values, and `undefined`.
## Possible Ways to Fix It
Return an array of params objects from `generateStaticParams`:
```tsx filename="app/blog/[slug]/page.tsx"
export function generateStaticParams() {
return [{ slug: 'first-post' }, { slug: 'second-post' }]
}
```
See the [`generateStaticParams` return value documentation](/docs/app/api-reference/functions/generate-static-params#returns) for the expected shape.
## Useful Links
- [`generateStaticParams` documentation](/docs/app/api-reference/functions/generate-static-params)
+3 -1
View File
@@ -1447,5 +1447,7 @@
"1446": "Missing initURL", "1446": "Missing initURL",
"1447": "Could not determine origin for forwarded Server Actions request. This can happen if port or hostname are not configured for this server.", "1447": "Could not determine origin for forwarded Server Actions request. This can happen if port or hostname are not configured for this server.",
"1448": "Error in the \"%s\" app-route HMR subscription", "1448": "Error in the \"%s\" app-route HMR subscription",
"1449": "Accessed `searchParams` during prerendering." "1449": "Accessed `searchParams` during prerendering.",
"1450": "Invalid value returned from generateStaticParams for \"%s\". Expected an array, but received type %s. See more info here: https://nextjs.org/docs/messages/generate-static-params",
"1451": "Invalid value at index %s returned from generateStaticParams for \"%s\". Expected an object, but received type %s. See more info here: https://nextjs.org/docs/messages/generate-static-params"
} }
@@ -1299,6 +1299,253 @@ describe('generateRouteStaticParams', () => {
).rejects.toThrow('Async error') ).rejects.toThrow('Async error')
}) })
it('should reject a non-array object return value', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => ({ id: '1' }) as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value returned from generateStaticParams for "/test-page". Expected an array, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a null return value', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => null as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value returned from generateStaticParams for "/test-page". Expected an array, but received type null. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject an undefined return value', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => undefined as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value returned from generateStaticParams for "/test-page". Expected an array, but received type undefined. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a non-array return value from a nested generateStaticParams', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [{ category: 'tech' }]),
createMockSegment(async () => undefined as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value returned from generateStaticParams for "/test-page". Expected an array, but received type undefined. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a null array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [null] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type null. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a string array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [''] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type string. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject an array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [[]] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type array. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject an undefined array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [undefined] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type undefined. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a number array entry from Date.now()', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [Date.now()] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type number. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a Date array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [new Date()] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a Map array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(
async () => [new Map([['slug', 'post']])] as unknown as Params[]
),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a Set array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(
async () => [new Set(['post'])] as unknown as Params[]
),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a RegExp array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [/post/] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a Promise array entry', async () => {
const segments: TestAppSegment[] = [
createMockSegment(
async () => [Promise.resolve({ slug: 'post' })] as unknown as Params[]
),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject a class instance array entry', async () => {
class StaticParams {
slug = 'post'
}
const segments: TestAppSegment[] = [
createMockSegment(
async () => [new StaticParams()] as unknown as Params[]
),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject an object with a custom prototype', async () => {
const params = Object.assign(Object.create({ inherited: true }), {
slug: 'post',
})
const segments: TestAppSegment[] = [
createMockSegment(async () => [params] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should reject an invalid array entry from a nested generateStaticParams', async () => {
const segments: TestAppSegment[] = [
createMockSegment(async () => [{ category: 'tech' }]),
createMockSegment(async () => [null] as unknown as Params[]),
]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).rejects.toThrow(
'Invalid value at index 0 returned from generateStaticParams for "/test-page". Expected an object, but received type null. See more info here: https://nextjs.org/docs/messages/generate-static-params'
)
})
it('should allow an empty params object', async () => {
const segments: TestAppSegment[] = [createMockSegment(async () => [{}])]
const store = createMockWorkStore()
await expect(
generateRouteStaticParams(segments, store, false, [])
).resolves.toEqual([{}])
})
it('should handle partially failing generateStaticParams', async () => { it('should handle partially failing generateStaticParams', async () => {
const segments: TestAppSegment[] = [ const segments: TestAppSegment[] = [
createMockSegment(async () => [{ category: 'tech' }]), createMockSegment(async () => [{ category: 'tech' }]),
+31 -3
View File
@@ -31,6 +31,7 @@ import { throwEmptyGenerateStaticParamsError } from '../../shared/lib/errors/emp
import type { AppRouteModule } from '../../server/route-modules/app-route/module.compiled' import type { AppRouteModule } from '../../server/route-modules/app-route/module.compiled'
import type { NormalizedAppRoute } from '../../shared/lib/router/routes/app' import type { NormalizedAppRoute } from '../../shared/lib/router/routes/app'
import { interceptionPrefixFromParamType } from '../../shared/lib/router/utils/interception-prefix-from-param-type' import { interceptionPrefixFromParamType } from '../../shared/lib/router/utils/interception-prefix-from-param-type'
import { isPlainObject } from '../../shared/lib/is-plain-object'
import { import {
type GenerateStaticParamsStore, type GenerateStaticParamsStore,
workUnitAsyncStorage, workUnitAsyncStorage,
@@ -598,11 +599,18 @@ export function assignStaticShellMetadata(
} }
} }
function getValueType(value: unknown): string {
if (value === null) return 'null'
if (Array.isArray(value)) return 'array'
return typeof value
}
/** /**
* Calls a single generateStaticParams function within a WorkUnitStore context, * Calls a single generateStaticParams function within a WorkUnitStore context,
* making root param getters available during static param generation. * making root param getters available during static param generation.
*/ */
async function callGenerateStaticParams( async function callGenerateStaticParams(
page: string,
generateStaticParams: NonNullable<AppSegment['generateStaticParams']>, generateStaticParams: NonNullable<AppSegment['generateStaticParams']>,
parentParams: Params, parentParams: Params,
rootParamKeys: readonly string[], rootParamKeys: readonly string[],
@@ -622,9 +630,27 @@ async function callGenerateStaticParams(
rootParams, rootParams,
} }
return workUnitAsyncStorage.run(workUnitStore, generateStaticParams, { const generatedParams: unknown = await workUnitAsyncStorage.run(
params: parentParams, workUnitStore,
}) generateStaticParams,
{ params: parentParams }
)
if (!Array.isArray(generatedParams)) {
throw new Error(
`Invalid value returned from generateStaticParams for "${page}". Expected an array, but received type ${getValueType(generatedParams)}. See more info here: https://nextjs.org/docs/messages/generate-static-params`
)
}
for (const [index, params] of generatedParams.entries()) {
if (!isPlainObject(params)) {
throw new Error(
`Invalid value at index ${index} returned from generateStaticParams for "${page}". Expected an object, but received type ${getValueType(params)}. See more info here: https://nextjs.org/docs/messages/generate-static-params`
)
}
}
return generatedParams
} }
/** /**
@@ -695,6 +721,7 @@ export async function generateRouteStaticParams(
// Process each parent parameter combination // Process each parent parameter combination
for (const parentParams of params) { for (const parentParams of params) {
const result = await callGenerateStaticParams( const result = await callGenerateStaticParams(
store.page,
current.generateStaticParams, current.generateStaticParams,
parentParams, parentParams,
rootParamKeys, rootParamKeys,
@@ -716,6 +743,7 @@ export async function generateRouteStaticParams(
} else { } else {
// No parent params, call generateStaticParams with empty object // No parent params, call generateStaticParams with empty object
const result = await callGenerateStaticParams( const result = await callGenerateStaticParams(
store.page,
current.generateStaticParams, current.generateStaticParams,
{}, {},
rootParamKeys, rootParamKeys,
@@ -12,6 +12,24 @@ describe('app dir - with output export - dynamic missing gsp', () => {
}) })
}) })
describe('should error when generateStaticParams returns a non-array', () => {
runTests({
dynamicPage: 'undefined',
generateStaticParamsOpt: 'set non-array',
expectedErrMsg:
'Invalid value returned from generateStaticParams for "/another/[slug]". Expected an array, but received type object. See more info here: https://nextjs.org/docs/messages/generate-static-params',
})
})
describe('should error when generateStaticParams returns a non-object entry', () => {
runTests({
dynamicPage: 'undefined',
generateStaticParamsOpt: 'set invalid entry',
expectedErrMsg:
'Invalid value at index 0 returned from generateStaticParams for "/another/[slug]". Expected an object, but received type null. See more info here: https://nextjs.org/docs/messages/generate-static-params',
})
})
describe('should error when client component has generateStaticParams', () => { describe('should error when client component has generateStaticParams', () => {
const expectedErrMsg = process.env.IS_TURBOPACK_TEST const expectedErrMsg = process.env.IS_TURBOPACK_TEST
? 'App pages cannot use both "use client" and export function "generateStaticParams()".' ? 'App pages cannot use both "use client" and export function "generateStaticParams()".'
+19 -1
View File
@@ -217,7 +217,11 @@ export function runTests({
dynamicPage?: string dynamicPage?: string
dynamicParams?: string dynamicParams?: string
dynamicApiRoute?: string dynamicApiRoute?: string
generateStaticParamsOpt?: 'set noop' | 'set client' generateStaticParamsOpt?:
| 'set noop'
| 'set client'
| 'set invalid entry'
| 'set non-array'
expectedErrMsg?: string | RegExp expectedErrMsg?: string | RegExp
}) { }) {
let { next, skipped, isNextDev } = nextTestSetup({ let { next, skipped, isNextDev } = nextTestSetup({
@@ -276,6 +280,20 @@ export function runTests({
'app/another/[slug]/page.js', 'app/another/[slug]/page.js',
(content) => '"use client"\n' + content (content) => '"use client"\n' + content
) )
} else if (generateStaticParamsOpt === 'set non-array') {
await next.patchFile('app/another/[slug]/page.js', (content) =>
content.replace(
`return [{ slug: 'first' }, { slug: 'second' }]`,
`return { slug: 'first' }`
)
)
} else if (generateStaticParamsOpt === 'set invalid entry') {
await next.patchFile('app/another/[slug]/page.js', (content) =>
content.replace(
`return [{ slug: 'first' }, { slug: 'second' }]`,
`return [null]`
)
)
} }
}) })