fix: ensure route handlers properly track dynamic access (#66446)
In #60645, dynamic access tracking was refactored but we erroneously stopped tracking dynamic access in route handlers. Request proxying, as well as tracking segment level configs (such as `export const dynamic = 'force-dynamic'`), were only enabled during static generation. This was an unintended breaking change that consequently caused dynamic access to not properly bail from data cache in various circumstances. This adds some more rigorous testing for route handlers, as this seems to be a fairly large gap in our fetch cache testing currently. This PR is easiest to review with [whitespace disabled](https://github.com/vercel/next.js/pull/66446/files?w=1).
This commit is contained in:
parent
34c2a05da2
commit
734fa1df62
13 changed files with 376 additions and 138 deletions
|
@ -154,7 +154,7 @@ export function trackDynamicDataAccessed(
|
|||
if (store.isStaticGeneration) {
|
||||
// We aren't prerendering but we are generating a static page. We need to bail out of static generation
|
||||
const err = new DynamicServerError(
|
||||
`Route ${pathname} couldn't be rendered statically because it used ${expression}. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
|
||||
`Route ${pathname} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
|
||||
)
|
||||
store.dynamicUsageDescription = expression
|
||||
store.dynamicUsageStack = err.stack
|
||||
|
|
|
@ -43,7 +43,10 @@ import * as serverHooks from '../../../../client/components/hooks-server-context
|
|||
import { DynamicServerError } from '../../../../client/components/hooks-server-context'
|
||||
|
||||
import { requestAsyncStorage } from '../../../../client/components/request-async-storage.external'
|
||||
import { staticGenerationAsyncStorage } from '../../../../client/components/static-generation-async-storage.external'
|
||||
import {
|
||||
staticGenerationAsyncStorage,
|
||||
type StaticGenerationStore,
|
||||
} from '../../../../client/components/static-generation-async-storage.external'
|
||||
import { actionAsyncStorage } from '../../../../client/components/action-async-storage.external'
|
||||
import * as sharedModules from './shared-modules'
|
||||
import { getIsServerAction } from '../../../lib/server-action-request-meta'
|
||||
|
@ -51,6 +54,7 @@ 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'
|
||||
import { trackDynamicDataAccessed } from '../../../app-render/dynamic-rendering'
|
||||
|
||||
/**
|
||||
* The AppRouteModule is the type of the module exported by the bundled App
|
||||
|
@ -317,7 +321,6 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
let request = rawRequest
|
||||
|
||||
// Update the static generation store based on the dynamic property.
|
||||
if (isStaticGeneration) {
|
||||
switch (this.dynamic) {
|
||||
case 'force-dynamic': {
|
||||
// Routes of generated paths should be dynamic
|
||||
|
@ -330,10 +333,7 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
staticGenerationStore.forceStatic = true
|
||||
// We also Proxy the request to replace dynamic data on the request
|
||||
// with empty stubs to allow for safely executing as static
|
||||
request = new Proxy(
|
||||
rawRequest,
|
||||
forceStaticRequestHandlers
|
||||
)
|
||||
request = new Proxy(rawRequest, forceStaticRequestHandlers)
|
||||
break
|
||||
case 'error':
|
||||
// The dynamic property is set to error, so we should throw an
|
||||
|
@ -346,25 +346,12 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
)
|
||||
break
|
||||
default:
|
||||
// When we are statically generating a route we want to bail out if anything dynamic
|
||||
// is accessed. We only create this proxy in the staticGenerationCase because it is overhead
|
||||
// for dynamic runtime executions
|
||||
request = new Proxy(
|
||||
// We proxy `NextRequest` to track dynamic access, and potentially bail out of static generation
|
||||
request = proxyNextRequest(
|
||||
rawRequest,
|
||||
staticGenerationRequestHandlers
|
||||
staticGenerationStore
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Generally if we are in a dynamic render we don't have to modify much however for
|
||||
// force-static specifically we ensure the dynamic and static behavior is consistent
|
||||
// by proxying the request in the same way in both cases
|
||||
if (this.dynamic === 'force-static') {
|
||||
// The dynamic property is set to force-static, so we should
|
||||
// force the page to be static.
|
||||
staticGenerationStore.forceStatic = true
|
||||
request = new Proxy(rawRequest, forceStaticRequestHandlers)
|
||||
}
|
||||
}
|
||||
|
||||
// If the static generation store does not have a revalidate value
|
||||
// set, then we should set it the revalidate value from the userland
|
||||
|
@ -672,7 +659,48 @@ const forceStaticNextUrlHandlers = {
|
|||
},
|
||||
}
|
||||
|
||||
const staticGenerationRequestHandlers = {
|
||||
function proxyNextRequest(
|
||||
request: NextRequest,
|
||||
staticGenerationStore: StaticGenerationStore
|
||||
) {
|
||||
const nextUrlHandlers = {
|
||||
get(
|
||||
target: NextURL & UrlSymbolTarget,
|
||||
prop: string | symbol,
|
||||
receiver: any
|
||||
): unknown {
|
||||
switch (prop) {
|
||||
case 'search':
|
||||
case 'searchParams':
|
||||
case 'url':
|
||||
case 'href':
|
||||
case 'toJSON':
|
||||
case 'toString':
|
||||
case 'origin': {
|
||||
trackDynamicDataAccessed(staticGenerationStore, `nextUrl.${prop}`)
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
if (typeof result === 'function') {
|
||||
return result.bind(target)
|
||||
}
|
||||
return result
|
||||
}
|
||||
case 'clone':
|
||||
return (
|
||||
target[urlCloneSymbol] ||
|
||||
(target[urlCloneSymbol] = () =>
|
||||
new Proxy(target.clone(), nextUrlHandlers))
|
||||
)
|
||||
default:
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
if (typeof result === 'function') {
|
||||
return result.bind(target)
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const nextRequestHandlers = {
|
||||
get(
|
||||
target: NextRequest & RequestSymbolTarget,
|
||||
prop: string | symbol,
|
||||
|
@ -682,10 +710,7 @@ const staticGenerationRequestHandlers = {
|
|||
case 'nextUrl':
|
||||
return (
|
||||
target[nextURLSymbol] ||
|
||||
(target[nextURLSymbol] = new Proxy(
|
||||
target.nextUrl,
|
||||
staticGenerationNextUrlHandlers
|
||||
))
|
||||
(target[nextURLSymbol] = new Proxy(target.nextUrl, nextUrlHandlers))
|
||||
)
|
||||
case 'headers':
|
||||
case 'cookies':
|
||||
|
@ -695,10 +720,15 @@ const staticGenerationRequestHandlers = {
|
|||
case 'json':
|
||||
case 'text':
|
||||
case 'arrayBuffer':
|
||||
case 'formData':
|
||||
throw new DynamicServerError(
|
||||
`Route ${target.nextUrl.pathname} couldn't be rendered statically because it accessed \`request.${prop}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
|
||||
)
|
||||
case 'formData': {
|
||||
trackDynamicDataAccessed(staticGenerationStore, `request.${prop}`)
|
||||
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
if (typeof result === 'function') {
|
||||
return result.bind(target)
|
||||
}
|
||||
return result
|
||||
}
|
||||
case 'clone':
|
||||
return (
|
||||
target[requestCloneSymbol] ||
|
||||
|
@ -712,7 +742,7 @@ const staticGenerationRequestHandlers = {
|
|||
// to probably embed the static generation logic into the class itself removing the need
|
||||
// for any kind of proxying
|
||||
target.clone() as NextRequest,
|
||||
staticGenerationRequestHandlers
|
||||
nextRequestHandlers
|
||||
))
|
||||
)
|
||||
default:
|
||||
|
@ -725,39 +755,9 @@ const staticGenerationRequestHandlers = {
|
|||
},
|
||||
// We don't need to proxy set because all the properties we proxy are ready only
|
||||
// and will be ignored
|
||||
}
|
||||
}
|
||||
|
||||
const staticGenerationNextUrlHandlers = {
|
||||
get(
|
||||
target: NextURL & UrlSymbolTarget,
|
||||
prop: string | symbol,
|
||||
receiver: any
|
||||
): unknown {
|
||||
switch (prop) {
|
||||
case 'search':
|
||||
case 'searchParams':
|
||||
case 'url':
|
||||
case 'href':
|
||||
case 'toJSON':
|
||||
case 'toString':
|
||||
case 'origin':
|
||||
throw new DynamicServerError(
|
||||
`Route ${target.pathname} couldn't be rendered statically because it accessed \`nextUrl.${prop}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
|
||||
)
|
||||
case 'clone':
|
||||
return (
|
||||
target[urlCloneSymbol] ||
|
||||
(target[urlCloneSymbol] = () =>
|
||||
new Proxy(target.clone(), staticGenerationNextUrlHandlers))
|
||||
)
|
||||
default:
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
if (typeof result === 'function') {
|
||||
return result.bind(target)
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
return new Proxy(request, nextRequestHandlers)
|
||||
}
|
||||
|
||||
const requireStaticRequestHandlers = {
|
||||
|
@ -785,7 +785,7 @@ const requireStaticRequestHandlers = {
|
|||
case 'arrayBuffer':
|
||||
case 'formData':
|
||||
throw new StaticGenBailoutError(
|
||||
`Route ${target.nextUrl.pathname} with \`dynamic = "error"\` couldn't be rendered statically because it accessed \`request.${prop}\`.`
|
||||
`Route ${target.nextUrl.pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`request.${prop}\`.`
|
||||
)
|
||||
case 'clone':
|
||||
return (
|
||||
|
@ -830,7 +830,7 @@ const requireStaticNextUrlHandlers = {
|
|||
case 'toString':
|
||||
case 'origin':
|
||||
throw new StaticGenBailoutError(
|
||||
`Route ${target.pathname} with \`dynamic = "error"\` couldn't be rendered statically because it accessed \`nextUrl.${prop}\`.`
|
||||
`Route ${target.pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`nextUrl.${prop}\`.`
|
||||
)
|
||||
case 'clone':
|
||||
return (
|
||||
|
|
|
@ -374,11 +374,15 @@ function createPatchedFetcher(
|
|||
getRequestMeta('method')?.toLowerCase() || 'get'
|
||||
)
|
||||
|
||||
// if there are authorized headers or a POST method and
|
||||
// dynamic data usage was present above the tree we bail
|
||||
// e.g. if cookies() is used before an authed/POST fetch
|
||||
// or no user provided fetch cache config or revalidate
|
||||
// is provided we don't cache
|
||||
/**
|
||||
* We automatically disable fetch caching under the following conditions:
|
||||
* - Fetch cache configs are not set. Specifically:
|
||||
* - A page fetch cache mode is not set (export const fetchCache=...)
|
||||
* - A fetch cache mode is not set in the fetch call (fetch(url, { cache: ... }))
|
||||
* - A fetch revalidate value is not set in the fetch call (fetch(url, { revalidate: ... }))
|
||||
* - OR the fetch comes after a configuration that triggered dynamic rendering (e.g., reading cookies())
|
||||
* and the fetch was considered uncacheable (e.g., POST method or has authorization headers)
|
||||
*/
|
||||
const autoNoCache =
|
||||
// this condition is hit for null/undefined
|
||||
// eslint-disable-next-line eqeqeq
|
||||
|
|
|
@ -154,6 +154,44 @@ describe('app-dir static/dynamic handling', () => {
|
|||
expect($2('#data').text()).toBeTruthy()
|
||||
expect($2('#data').text()).not.toBe(initData)
|
||||
})
|
||||
|
||||
// Check route handlers as well
|
||||
const initFetchData = await (
|
||||
await next.fetch('/force-dynamic-fetch-cache/no-fetch-cache/route')
|
||||
).json()
|
||||
|
||||
await retry(async () => {
|
||||
const newFetchData = await (
|
||||
await next.fetch('/force-dynamic-fetch-cache/no-fetch-cache/route')
|
||||
).json()
|
||||
expect(newFetchData).toBeTruthy()
|
||||
expect(newFetchData).not.toEqual(initFetchData)
|
||||
})
|
||||
})
|
||||
|
||||
it('force-dynamic should supercede a "default" cache value', async () => {
|
||||
const $ = await next.render$('/force-dynamic-fetch-cache/default-cache')
|
||||
const initData = $('#data').text()
|
||||
await retry(async () => {
|
||||
const $2 = await next.render$(
|
||||
'/force-dynamic-fetch-cache/default-cache'
|
||||
)
|
||||
expect($2('#data').text()).toBeTruthy()
|
||||
expect($2('#data').text()).not.toBe(initData)
|
||||
})
|
||||
|
||||
// Check route handlers as well
|
||||
const initFetchData = await (
|
||||
await next.fetch('/force-dynamic-fetch-cache/default-cache/route')
|
||||
).json()
|
||||
|
||||
await retry(async () => {
|
||||
const newFetchData = await (
|
||||
await next.fetch('/force-dynamic-fetch-cache/default-cache/route')
|
||||
).json()
|
||||
expect(newFetchData).toBeTruthy()
|
||||
expect(newFetchData).not.toEqual(initFetchData)
|
||||
})
|
||||
})
|
||||
|
||||
it('fetchCache config should supercede dynamic config when force-dynamic is used', async () => {
|
||||
|
@ -168,6 +206,42 @@ describe('app-dir static/dynamic handling', () => {
|
|||
expect($2('#data').text()).toBeTruthy()
|
||||
expect($2('#data').text()).toBe(initData)
|
||||
})
|
||||
|
||||
// Check route handlers as well
|
||||
const initFetchData = await (
|
||||
await next.fetch('/force-dynamic-fetch-cache/with-fetch-cache/route')
|
||||
).json()
|
||||
|
||||
await retry(async () => {
|
||||
const newFetchData = await (
|
||||
await next.fetch('/force-dynamic-fetch-cache/with-fetch-cache/route')
|
||||
).json()
|
||||
expect(newFetchData).toBeTruthy()
|
||||
expect(newFetchData).toEqual(initFetchData)
|
||||
})
|
||||
})
|
||||
|
||||
it('fetch `cache` should supercede dynamic config when force-dynamic is used', async () => {
|
||||
const $ = await next.render$('/force-dynamic-fetch-cache/force-cache')
|
||||
const initData = $('#data').text()
|
||||
await retry(async () => {
|
||||
const $2 = await next.render$('/force-dynamic-fetch-cache/force-cache')
|
||||
expect($2('#data').text()).toBeTruthy()
|
||||
expect($2('#data').text()).toBe(initData)
|
||||
})
|
||||
|
||||
// Check route handlers as well
|
||||
const initFetchData = await (
|
||||
await next.fetch('/force-dynamic-fetch-cache/force-cache/route')
|
||||
).json()
|
||||
|
||||
await retry(async () => {
|
||||
const newFetchData = await (
|
||||
await next.fetch('/force-dynamic-fetch-cache/force-cache/route')
|
||||
).json()
|
||||
expect(newFetchData).toBeTruthy()
|
||||
expect(newFetchData).toEqual(initFetchData)
|
||||
})
|
||||
})
|
||||
|
||||
if (!process.env.CUSTOM_CACHE_HANDLER) {
|
||||
|
@ -725,10 +799,18 @@ describe('app-dir static/dynamic handling', () => {
|
|||
"force-cache/page_client-reference-manifest.js",
|
||||
"force-dynamic-catch-all/[slug]/[[...id]]/page.js",
|
||||
"force-dynamic-catch-all/[slug]/[[...id]]/page_client-reference-manifest.js",
|
||||
"force-dynamic-fetch-cache/default-cache/page.js",
|
||||
"force-dynamic-fetch-cache/default-cache/page_client-reference-manifest.js",
|
||||
"force-dynamic-fetch-cache/default-cache/route/route.js",
|
||||
"force-dynamic-fetch-cache/force-cache/page.js",
|
||||
"force-dynamic-fetch-cache/force-cache/page_client-reference-manifest.js",
|
||||
"force-dynamic-fetch-cache/force-cache/route/route.js",
|
||||
"force-dynamic-fetch-cache/no-fetch-cache/page.js",
|
||||
"force-dynamic-fetch-cache/no-fetch-cache/page_client-reference-manifest.js",
|
||||
"force-dynamic-fetch-cache/no-fetch-cache/route/route.js",
|
||||
"force-dynamic-fetch-cache/with-fetch-cache/page.js",
|
||||
"force-dynamic-fetch-cache/with-fetch-cache/page_client-reference-manifest.js",
|
||||
"force-dynamic-fetch-cache/with-fetch-cache/route/route.js",
|
||||
"force-dynamic-no-prerender/[id]/page.js",
|
||||
"force-dynamic-no-prerender/[id]/page_client-reference-manifest.js",
|
||||
"force-dynamic-prerender/[slug]/page.js",
|
||||
|
@ -896,6 +978,8 @@ describe('app-dir static/dynamic handling', () => {
|
|||
"variable-revalidate/authorization.rsc",
|
||||
"variable-revalidate/authorization/page.js",
|
||||
"variable-revalidate/authorization/page_client-reference-manifest.js",
|
||||
"variable-revalidate/authorization/route-cookies/route.js",
|
||||
"variable-revalidate/authorization/route-request/route.js",
|
||||
"variable-revalidate/cookie.html",
|
||||
"variable-revalidate/cookie.rsc",
|
||||
"variable-revalidate/cookie/page.js",
|
||||
|
@ -2104,6 +2188,10 @@ describe('app-dir static/dynamic handling', () => {
|
|||
expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe(
|
||||
prev$('#data-revalidate-and-fetch-cache').text()
|
||||
)
|
||||
|
||||
expect(cur$('#data-auto-cache').text()).not.toBe(
|
||||
prev$('data-auto-cache').text()
|
||||
)
|
||||
} finally {
|
||||
prevHtml = curHtml
|
||||
prev$ = cur$
|
||||
|
@ -2562,6 +2650,38 @@ describe('app-dir static/dynamic handling', () => {
|
|||
}, 'success')
|
||||
})
|
||||
|
||||
it('should skip fetch cache when an authorization header is present after dynamic usage', async () => {
|
||||
const initialReq = await next.fetch(
|
||||
'/variable-revalidate/authorization/route-cookies'
|
||||
)
|
||||
const initialJson = await initialReq.json()
|
||||
|
||||
await retry(async () => {
|
||||
const req = await next.fetch(
|
||||
'/variable-revalidate/authorization/route-cookies'
|
||||
)
|
||||
const json = await req.json()
|
||||
|
||||
expect(json).not.toEqual(initialJson)
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip fetch cache when accessing request properties', async () => {
|
||||
const initialReq = await next.fetch(
|
||||
'/variable-revalidate/authorization/route-request'
|
||||
)
|
||||
const initialJson = await initialReq.json()
|
||||
|
||||
await retry(async () => {
|
||||
const req = await next.fetch(
|
||||
'/variable-revalidate/authorization/route-request'
|
||||
)
|
||||
const json = await req.json()
|
||||
|
||||
expect(json).not.toEqual(initialJson)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not cache correctly with POST method request init', async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Page() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{ cache: 'default' }
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="page">/force-dynamic-fetch-cache/default-cache</p>
|
||||
<p id="data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{ cache: 'default' }
|
||||
).then((res) => res.text())
|
||||
|
||||
return NextResponse.json({ random: data })
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Page() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{ cache: 'force-cache' }
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="page">/force-dynamic-fetch-cache/force-cache</p>
|
||||
<p id="data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{ cache: 'force-cache' }
|
||||
).then((res) => res.text())
|
||||
|
||||
return NextResponse.json({ random: data })
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random'
|
||||
).then((res) => res.text())
|
||||
|
||||
return NextResponse.json({ random: data })
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const fetchCache = 'force-cache'
|
||||
|
||||
export async function GET() {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random'
|
||||
).then((res) => res.text())
|
||||
|
||||
return NextResponse.json({ random: data })
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { cookies } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
const cookieData = cookies()
|
||||
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
console.log('has cookie data', !!cookieData)
|
||||
|
||||
return NextResponse.json({ random: data })
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(request) {
|
||||
const requestCookies = request.cookies
|
||||
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
console.log('has cookie data', !!requestCookies)
|
||||
|
||||
return NextResponse.json({ random: data })
|
||||
}
|
|
@ -166,10 +166,10 @@ describe('dynamic-data', () => {
|
|||
if (!isNextDev) {
|
||||
it('should track dynamic apis when rendering app routes', async () => {
|
||||
expect(next.cliOutput).toContain(
|
||||
`Caught Error: Dynamic server usage: Route /routes/url couldn't be rendered statically because it accessed \`request.url\`.`
|
||||
`Caught Error: Dynamic server usage: Route /routes/url couldn't be rendered statically because it used \`request.url\`.`
|
||||
)
|
||||
expect(next.cliOutput).toContain(
|
||||
`Caught Error: Dynamic server usage: Route /routes/next-url couldn't be rendered statically because it accessed \`nextUrl.toString\`.`
|
||||
`Caught Error: Dynamic server usage: Route /routes/next-url couldn't be rendered statically because it used \`nextUrl.toString\`.`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -244,10 +244,10 @@ describe('dynamic-data with dynamic = "error"', () => {
|
|||
'Error: Route /search with `dynamic = "error"` couldn\'t be rendered statically because it used `searchParams`.'
|
||||
)
|
||||
expect(next.cliOutput).toMatch(
|
||||
'Error: Route /routes/form-data/error with `dynamic = "error"` couldn\'t be rendered statically because it accessed `request.formData`.'
|
||||
'Error: Route /routes/form-data/error with `dynamic = "error"` couldn\'t be rendered statically because it used `request.formData`.'
|
||||
)
|
||||
expect(next.cliOutput).toMatch(
|
||||
'Error: Route /routes/next-url/error with `dynamic = "error"` couldn\'t be rendered statically because it accessed `nextUrl.toString`.'
|
||||
'Error: Route /routes/next-url/error with `dynamic = "error"` couldn\'t be rendered statically because it used `nextUrl.toString`.'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue