Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Breaking] Disable automatic static generation for route handlers #65825

Merged
merged 12 commits into from
May 20, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@ In addition to supporting native [Request](https://developer.mozilla.org/docs/We

### Caching

Route Handlers are cached by default when using the `GET` method with the `Response` object.
Route Handlers are dynamic by default as of Next.js v15. To opt-in to caching for GET requests in route handlers you can specify via the following config:

```ts filename="app/items/route.ts" switcher
// opt in to caching the route handler
export const dynamic = 'force-static' // or 'error'
// or
export const revalidate = false // or a value > 0

export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
headers: {
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/export/routes/app-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
import { isDynamicUsageError } from '../helpers/is-dynamic-usage-error'
import { SERVER_DIRECTORY } from '../../shared/lib/constants'
import { hasNextSupport } from '../../telemetry/ci-info'
import { isStaticGenEnabled } from '../../server/future/route-modules/app-route/helpers/is-static-gen-enabled'
import type { ExperimentalConfig } from '../../server/config-shared'

export const enum ExportedAppRouteFiles {
Expand Down Expand Up @@ -87,6 +88,12 @@ export async function exportAppRoute(
try {
// Route module loading and handling.
const module = await RouteModuleLoader.load<AppRouteRouteModule>(filename)
const userland = module.userland

if (!isStaticGenEnabled(userland)) {
return { revalidate: 0 }
}

const response = await module.handle(request, context)

const isValidStatus = response.status < 400 || response.status === 404
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { AppRouteModule } from '../module.compiled'

// route handlers are only statically optimized if they define
// one of these top-level configs manually
// - dynamic = 'force-static'
// - dynamic = 'error'
// - revalidate > 0
// - revalidate = false
// - generateStaticParams
export function isStaticGenEnabled(
mod: AppRouteModule['routeModule']['userland']
) {
return (
mod.dynamic === 'force-static' ||
mod.dynamic === 'error' ||
mod.revalidate === false ||
(mod.revalidate !== undefined && mod.revalidate > 0) ||
typeof mod.generateStaticParams == 'function'
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { getIsServerAction } from '../../../lib/server-action-request-meta'
import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies'
import { cleanURL } from './helpers/clean-url'
import { StaticGenBailoutError } from '../../../../client/components/static-generation-bailout'
import { isStaticGenEnabled } from './helpers/is-static-gen-enabled'

/**
* The AppRouteModule is the type of the module exported by the bundled App
Expand Down Expand Up @@ -178,12 +179,16 @@ export class AppRouteRouteModule extends RouteModule<
// Get the dynamic property from the userland module.
this.dynamic = this.userland.dynamic
if (this.nextConfigOutput === 'export') {
if (!this.dynamic || this.dynamic === 'auto') {
this.dynamic = 'error'
} else if (this.dynamic === 'force-dynamic') {
if (this.dynamic === 'force-dynamic') {
throw new Error(
`export const dynamic = "force-dynamic" on page "${definition.pathname}" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export`
)
} else if (!isStaticGenEnabled(this.userland) && this.userland['GET']) {
throw new Error(
`export const dynamic = "force-static"/export const revalidate not configured on route "${definition.pathname}" with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export`
)
} else {
this.dynamic = 'error'
}
}

Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-routes/app/api/hello.json/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'

export const revalidate = 1

export const GET = (req: NextRequest) => {
return NextResponse.json({
pathname: req.nextUrl.pathname,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-static/app/api/draft-mode/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { draftMode } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'

export const revalidate = 1
ijjk marked this conversation as resolved.
Show resolved Hide resolved

export function GET(req: NextRequest) {
const status = req.nextUrl.searchParams.get('status')

Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-static/app/api/large-data/route.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NextResponse } from 'next/server'

export const revalidate = false

export async function GET() {
console.log('Load data')
return NextResponse.json({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'

export const revalidate = 1

export async function GET(req: NextRequest) {
const path = req.nextUrl.searchParams.get('path') || '/'
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NextResponse } from 'next/server'

export const revalidate = 10

export async function GET() {
const data360 = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NextResponse } from 'next/server'

export const revalidate = false

export function GET() {
const res = new NextResponse()
res.cookies.set('theme', 'light')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const revalidate = 1

export const GET = async (request) => {
try {
const body = JSON.stringify({ pathname: request.nextUrl.toString() })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const revalidate = 1

export const GET = async (request) => {
try {
const body = JSON.stringify({ url: request.url })
Expand Down
2 changes: 2 additions & 0 deletions test/integration/app-dir-export/app/api/txt/route.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const revalidate = false

export async function GET() {
return new Response('this is plain text')
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ describe('app dir - with output export - dynamic api route dev', () => {
'development mode',
() => {
it.each([
{ dynamicApiRoute: 'undefined' },
{
dynamicApiRoute: 'undefined',
expectedErrMsg:
'export const dynamic = "force-static"/export const revalidate not configured on route',
},
{ dynamicApiRoute: "'error'" },
{ dynamicApiRoute: "'force-static'" },
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ describe('app dir - with output export - dynamic api route prod', () => {
'production mode',
() => {
it.each([
{ dynamicApiRoute: 'undefined' },
{
dynamicApiRoute: 'undefined',
expectedErrMsg:
'export const dynamic = "force-static"/export const revalidate not configured on route',
},
{ dynamicApiRoute: "'error'" },
{ dynamicApiRoute: "'force-static'" },
{
Expand Down
Loading