Update middleware query hydration handling (#41243)

This updates to skip the data request done during query hydration when
middleware is present as it was mainly to gather query params from any
potential rewrites in middleware although this is usually not needed for
static pages and the context can be gathered in different ways on the
client.

x-ref: [slack
thread](https://vercel.slack.com/archives/C045FKE5P51/p1665082474010149)

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`
This commit is contained in:
JJ Kasper 2022-10-10 12:58:18 -07:00 committed by GitHub
parent 05498a0988
commit 35308c668e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 99 additions and 62 deletions

View file

@ -253,6 +253,9 @@ export function getDefineEnv({
'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n), 'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n),
'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains), 'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains),
'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId), 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId),
'process.env.__NEXT_ALLOW_MIDDLEWARE_RESPONSE_BODY': JSON.stringify(
config.experimental.dangerouslyAllowMiddlewareResponseBody
),
'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': JSON.stringify( 'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': JSON.stringify(
config.experimental.skipMiddlewareUrlNormalize config.experimental.skipMiddlewareUrlNormalize
), ),
@ -1909,6 +1912,8 @@ export default async function getBaseWebpackConfig(
dev, dev,
sriEnabled: !dev && !!config.experimental.sri?.algorithm, sriEnabled: !dev && !!config.experimental.sri?.algorithm,
hasFontLoaders: !!config.experimental.fontLoaders, hasFontLoaders: !!config.experimental.fontLoaders,
dangerouslyAllowMiddlewareResponseBody:
!!config.experimental.dangerouslyAllowMiddlewareResponseBody,
}), }),
isClient && isClient &&
new BuildManifestPlugin({ new BuildManifestPlugin({

View file

@ -340,12 +340,14 @@ function getCodeAnalyzer(params: {
dev: boolean dev: boolean
compiler: webpack.Compiler compiler: webpack.Compiler
compilation: webpack.Compilation compilation: webpack.Compilation
dangerouslyAllowMiddlewareResponseBody: boolean
}) { }) {
return (parser: webpack.javascript.JavascriptParser) => { return (parser: webpack.javascript.JavascriptParser) => {
const { const {
dev, dev,
compiler: { webpack: wp }, compiler: { webpack: wp },
compilation, compilation,
dangerouslyAllowMiddlewareResponseBody,
} = params } = params
const { hooks } = parser const { hooks } = parser
@ -560,8 +562,11 @@ Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`,
.for(`${prefix}WebAssembly.instantiate`) .for(`${prefix}WebAssembly.instantiate`)
.tap(NAME, handleWrapWasmInstantiateExpression) .tap(NAME, handleWrapWasmInstantiateExpression)
} }
hooks.new.for('Response').tap(NAME, handleNewResponseExpression)
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression) if (!dangerouslyAllowMiddlewareResponseBody) {
hooks.new.for('Response').tap(NAME, handleNewResponseExpression)
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
}
hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain) hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain)
hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain) hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
hooks.importCall.tap(NAME, handleImport) hooks.importCall.tap(NAME, handleImport)
@ -801,19 +806,24 @@ export default class MiddlewarePlugin {
private readonly dev: boolean private readonly dev: boolean
private readonly sriEnabled: boolean private readonly sriEnabled: boolean
private readonly hasFontLoaders: boolean private readonly hasFontLoaders: boolean
private readonly dangerouslyAllowMiddlewareResponseBody: boolean
constructor({ constructor({
dev, dev,
sriEnabled, sriEnabled,
hasFontLoaders, hasFontLoaders,
dangerouslyAllowMiddlewareResponseBody,
}: { }: {
dev: boolean dev: boolean
sriEnabled: boolean sriEnabled: boolean
hasFontLoaders: boolean hasFontLoaders: boolean
dangerouslyAllowMiddlewareResponseBody: boolean
}) { }) {
this.dev = dev this.dev = dev
this.sriEnabled = sriEnabled this.sriEnabled = sriEnabled
this.hasFontLoaders = hasFontLoaders this.hasFontLoaders = hasFontLoaders
this.dangerouslyAllowMiddlewareResponseBody =
dangerouslyAllowMiddlewareResponseBody
} }
public apply(compiler: webpack.Compiler) { public apply(compiler: webpack.Compiler) {
@ -826,6 +836,8 @@ export default class MiddlewarePlugin {
dev: this.dev, dev: this.dev,
compiler, compiler,
compilation, compilation,
dangerouslyAllowMiddlewareResponseBody:
this.dangerouslyAllowMiddlewareResponseBody,
}) })
hooks.parser.for('javascript/auto').tap(NAME, codeAnalyzer) hooks.parser.for('javascript/auto').tap(NAME, codeAnalyzer)
hooks.parser.for('javascript/dynamic').tap(NAME, codeAnalyzer) hooks.parser.for('javascript/dynamic').tap(NAME, codeAnalyzer)

View file

@ -268,6 +268,9 @@ const configSchema = {
appDir: { appDir: {
type: 'boolean', type: 'boolean',
}, },
dangerouslyAllowMiddlewareResponseBody: {
type: 'boolean',
},
externalDir: { externalDir: {
type: 'boolean', type: 'boolean',
}, },
@ -323,12 +326,6 @@ const configSchema = {
optimisticClientCache: { optimisticClientCache: {
type: 'boolean', type: 'boolean',
}, },
serverComponentsExternalPackages: {
items: {
type: 'string',
},
type: 'array',
},
outputFileTracingRoot: { outputFileTracingRoot: {
minLength: 1, minLength: 1,
type: 'string', type: 'string',
@ -348,6 +345,12 @@ const configSchema = {
enum: ['experimental-edge', 'nodejs'] as any, enum: ['experimental-edge', 'nodejs'] as any,
type: 'string', type: 'string',
}, },
serverComponentsExternalPackages: {
items: {
type: 'string',
},
type: 'array',
},
scrollRestoration: { scrollRestoration: {
type: 'boolean', type: 'boolean',
}, },

View file

@ -79,6 +79,7 @@ export interface NextJsWebpackConfig {
} }
export interface ExperimentalConfig { export interface ExperimentalConfig {
dangerouslyAllowMiddlewareResponseBody?: boolean
skipMiddlewareUrlNormalize?: boolean skipMiddlewareUrlNormalize?: boolean
skipTrailingSlashRedirect?: boolean skipTrailingSlashRedirect?: boolean
optimisticClientCache?: boolean optimisticClientCache?: boolean

View file

@ -186,6 +186,10 @@ export async function adapter(params: {
export function blockUnallowedResponse( export function blockUnallowedResponse(
promise: Promise<FetchEventResult> promise: Promise<FetchEventResult>
): Promise<FetchEventResult> { ): Promise<FetchEventResult> {
if (process.env.__NEXT_ALLOW_MIDDLEWARE_RESPONSE_BODY) {
return promise
}
return promise.then((result) => { return promise.then((result) => {
if (result.response?.body) { if (result.response?.body) {
console.error( console.error(

View file

@ -1187,7 +1187,7 @@ export default class Router implements BaseRouter {
// hydration. Your app should _never_ use this property. It may change at // hydration. Your app should _never_ use this property. It may change at
// any time without notice. // any time without notice.
const isQueryUpdating = (options as any)._h const isQueryUpdating = (options as any)._h
const shouldResolveHref = let shouldResolveHref =
isQueryUpdating || isQueryUpdating ||
(options as any)._shouldResolveHref || (options as any)._shouldResolveHref ||
parsePath(url).pathname === parsePath(as).pathname parsePath(url).pathname === parsePath(as).pathname
@ -1418,6 +1418,10 @@ export default class Router implements BaseRouter {
pathname = this.pathname pathname = this.pathname
} }
if (isQueryUpdating && isMiddlewareMatch) {
shouldResolveHref = false
}
if (shouldResolveHref && pathname !== '/_error') { if (shouldResolveHref && pathname !== '/_error') {
;(options as any)._shouldResolveHref = true ;(options as any)._shouldResolveHref = true
@ -1955,12 +1959,14 @@ export default class Router implements BaseRouter {
isBackground: isQueryUpdating, isBackground: isQueryUpdating,
} }
const data = await withMiddlewareEffects({ const data = isQueryUpdating
fetchData: () => fetchNextData(fetchNextDataParams), ? ({} as any)
asPath: resolvedAs, : await withMiddlewareEffects({
locale: locale, fetchData: () => fetchNextData(fetchNextDataParams),
router: this, asPath: resolvedAs,
}) locale: locale,
router: this,
})
if (isQueryUpdating && data) { if (isQueryUpdating && data) {
data.json = self.__NEXT_DATA__.props data.json = self.__NEXT_DATA__.props
@ -2079,7 +2085,8 @@ export default class Router implements BaseRouter {
if ( if (
!this.isPreview && !this.isPreview &&
routeInfo.__N_SSG && routeInfo.__N_SSG &&
process.env.NODE_ENV !== 'development' process.env.NODE_ENV !== 'development' &&
!isQueryUpdating
) { ) {
fetchNextData( fetchNextData(
Object.assign({}, fetchNextDataParams, { Object.assign({}, fetchNextDataParams, {

View file

@ -224,14 +224,13 @@ describe('Middleware Runtime', () => {
await check( await check(
() => browser.eval('document.documentElement.innerHTML'), () => browser.eval('document.documentElement.innerHTML'),
/"from":"middleware"/ /"slug":"hello"/
) )
await check(() => browser.elementByCss('body').text(), /\/to-ssg/) await check(() => browser.elementByCss('body').text(), /\/to-ssg/)
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
slug: 'hello', slug: 'hello',
from: 'middleware',
}) })
expect( expect(
JSON.parse(await browser.elementByCss('#props').text()).params JSON.parse(await browser.elementByCss('#props').text()).params

View file

@ -121,7 +121,6 @@ describe('Middleware Runtime trailing slash', () => {
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
slug: 'hello', slug: 'hello',
from: 'middleware',
}) })
expect( expect(
JSON.parse(await browser.elementByCss('#props').text()).params JSON.parse(await browser.elementByCss('#props').text()).params

View file

@ -23,5 +23,13 @@ export default function handler(req) {
return res return res
} }
if (req.nextUrl.pathname === '/middleware-response-body') {
return new Response('hello from middleware', {
headers: {
'x-from-middleware': 'true',
},
})
}
return NextResponse.next() return NextResponse.next()
} }

View file

@ -0,0 +1,27 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
skipTrailingSlashRedirect: true,
skipMiddlewareUrlNormalize: true,
dangerouslyAllowMiddlewareResponseBody: true,
},
async redirects() {
return [
{
source: '/redirect-me',
destination: '/another',
permanent: false,
},
]
},
async rewrites() {
return [
{
source: '/rewrite-me',
destination: '/another',
},
]
},
}
module.exports = nextConfig

View file

@ -11,33 +11,17 @@ describe('skip-trailing-slash-redirect', () => {
next = await createNext({ next = await createNext({
files: new FileRef(join(__dirname, 'app')), files: new FileRef(join(__dirname, 'app')),
dependencies: {}, dependencies: {},
nextConfig: {
experimental: {
skipTrailingSlashRedirect: true,
skipMiddlewareUrlNormalize: true,
},
async redirects() {
return [
{
source: '/redirect-me',
destination: '/another',
permanent: false,
},
]
},
async rewrites() {
return [
{
source: '/rewrite-me',
destination: '/another',
},
]
},
},
}) })
}) })
afterAll(() => next.destroy()) afterAll(() => next.destroy())
it('should allow response body from middleware with flag', async () => {
const res = await fetchViaHTTP(next.url, '/middleware-response-body')
expect(res.status).toBe(200)
expect(res.headers.get('x-from-middleware')).toBe('true')
expect(await res.text()).toBe('hello from middleware')
})
it('should merge cookies from middleware and API routes correctly', async () => { it('should merge cookies from middleware and API routes correctly', async () => {
const res = await fetchViaHTTP(next.url, '/api/test-cookie', undefined, { const res = await fetchViaHTTP(next.url, '/api/test-cookie', undefined, {
redirect: 'manual', redirect: 'manual',

View file

@ -47,11 +47,6 @@ function runTests({ dev, serverless }) {
const cacheKeys = await getCacheKeys() const cacheKeys = await getCacheKeys()
expect(cacheKeys).toEqual([ expect(cacheKeys).toEqual([
...(process.env.__MIDDLEWARE_TEST
? // data route is fetched with middleware due to query hydration
// since middleware matches the index route
['/_next/data/BUILD_ID/index.json']
: []),
'/_next/data/BUILD_ID/p1/p2/all-ssg/hello.json?rest=hello', '/_next/data/BUILD_ID/p1/p2/all-ssg/hello.json?rest=hello',
'/_next/data/BUILD_ID/p1/p2/all-ssg/hello1/hello2.json?rest=hello1&rest=hello2', '/_next/data/BUILD_ID/p1/p2/all-ssg/hello1/hello2.json?rest=hello1&rest=hello2',
'/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello.json?rest=hello', '/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello.json?rest=hello',
@ -97,7 +92,14 @@ function runTests({ dev, serverless }) {
await browser.waitForElementByCss(linkSelector) await browser.waitForElementByCss(linkSelector)
} }
const newCacheKeys = await getCacheKeys() const newCacheKeys = await getCacheKeys()
expect(newCacheKeys).toEqual(cacheKeys) expect(newCacheKeys).toEqual([
...(process.env.__MIDDLEWARE_TEST
? // data route is fetched with middleware due to query hydration
// since middleware matches the index route
['/_next/data/BUILD_ID/index.json']
: []),
...cacheKeys,
])
}) })
} }

View file

@ -3,7 +3,6 @@
import { join } from 'path' import { join } from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import webdriver from 'next-webdriver' import webdriver from 'next-webdriver'
import assert from 'assert'
import { check, findPort, killApp, nextBuild, nextStart } from 'next-test-utils' import { check, findPort, killApp, nextBuild, nextStart } from 'next-test-utils'
jest.setTimeout(1000 * 60 * 2) jest.setTimeout(1000 * 60 * 2)
@ -62,19 +61,6 @@ describe('Middleware Production Prefetch', () => {
}, 'yes') }, 'yes')
}) })
it(`prefetches data when it is ssg`, async () => {
const browser = await webdriver(context.appPort, `/`)
await browser.elementByCss('#made-up-link').moveTo()
await check(async () => {
const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`)
const mapped = hrefs.map((href) =>
new URL(href).pathname.replace(/^\/_next\/data\/[^/]+/, '')
)
assert.deepEqual(mapped, ['/index.json'])
return 'yes'
}, 'yes')
})
it(`prefetches provided path even if it will be rewritten`, async () => { it(`prefetches provided path even if it will be rewritten`, async () => {
const browser = await webdriver(context.appPort, `/`) const browser = await webdriver(context.appPort, `/`)
await browser.elementByCss('#ssg-page-2').moveTo() await browser.elementByCss('#ssg-page-2').moveTo()