Skip to content

Commit

Permalink
Update middleware query hydration handling (#41243)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
ijjk committed Oct 10, 2022
1 parent 05498a0 commit 35308c6
Show file tree
Hide file tree
Showing 13 changed files with 99 additions and 62 deletions.
5 changes: 5 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -253,6 +253,9 @@ export function getDefineEnv({
'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n),
'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains),
'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(
config.experimental.skipMiddlewareUrlNormalize
),
Expand Down Expand Up @@ -1909,6 +1912,8 @@ export default async function getBaseWebpackConfig(
dev,
sriEnabled: !dev && !!config.experimental.sri?.algorithm,
hasFontLoaders: !!config.experimental.fontLoaders,
dangerouslyAllowMiddlewareResponseBody:
!!config.experimental.dangerouslyAllowMiddlewareResponseBody,
}),
isClient &&
new BuildManifestPlugin({
Expand Down
16 changes: 14 additions & 2 deletions packages/next/build/webpack/plugins/middleware-plugin.ts
Expand Up @@ -340,12 +340,14 @@ function getCodeAnalyzer(params: {
dev: boolean
compiler: webpack.Compiler
compilation: webpack.Compilation
dangerouslyAllowMiddlewareResponseBody: boolean
}) {
return (parser: webpack.javascript.JavascriptParser) => {
const {
dev,
compiler: { webpack: wp },
compilation,
dangerouslyAllowMiddlewareResponseBody,
} = params
const { hooks } = parser

Expand Down Expand Up @@ -560,8 +562,11 @@ Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`,
.for(`${prefix}WebAssembly.instantiate`)
.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.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
hooks.importCall.tap(NAME, handleImport)
Expand Down Expand Up @@ -801,19 +806,24 @@ export default class MiddlewarePlugin {
private readonly dev: boolean
private readonly sriEnabled: boolean
private readonly hasFontLoaders: boolean
private readonly dangerouslyAllowMiddlewareResponseBody: boolean

constructor({
dev,
sriEnabled,
hasFontLoaders,
dangerouslyAllowMiddlewareResponseBody,
}: {
dev: boolean
sriEnabled: boolean
hasFontLoaders: boolean
dangerouslyAllowMiddlewareResponseBody: boolean
}) {
this.dev = dev
this.sriEnabled = sriEnabled
this.hasFontLoaders = hasFontLoaders
this.dangerouslyAllowMiddlewareResponseBody =
dangerouslyAllowMiddlewareResponseBody
}

public apply(compiler: webpack.Compiler) {
Expand All @@ -826,6 +836,8 @@ export default class MiddlewarePlugin {
dev: this.dev,
compiler,
compilation,
dangerouslyAllowMiddlewareResponseBody:
this.dangerouslyAllowMiddlewareResponseBody,
})
hooks.parser.for('javascript/auto').tap(NAME, codeAnalyzer)
hooks.parser.for('javascript/dynamic').tap(NAME, codeAnalyzer)
Expand Down
15 changes: 9 additions & 6 deletions packages/next/server/config-schema.ts
Expand Up @@ -268,6 +268,9 @@ const configSchema = {
appDir: {
type: 'boolean',
},
dangerouslyAllowMiddlewareResponseBody: {
type: 'boolean',
},
externalDir: {
type: 'boolean',
},
Expand Down Expand Up @@ -323,12 +326,6 @@ const configSchema = {
optimisticClientCache: {
type: 'boolean',
},
serverComponentsExternalPackages: {
items: {
type: 'string',
},
type: 'array',
},
outputFileTracingRoot: {
minLength: 1,
type: 'string',
Expand All @@ -348,6 +345,12 @@ const configSchema = {
enum: ['experimental-edge', 'nodejs'] as any,
type: 'string',
},
serverComponentsExternalPackages: {
items: {
type: 'string',
},
type: 'array',
},
scrollRestoration: {
type: 'boolean',
},
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/config-shared.ts
Expand Up @@ -79,6 +79,7 @@ export interface NextJsWebpackConfig {
}

export interface ExperimentalConfig {
dangerouslyAllowMiddlewareResponseBody?: boolean
skipMiddlewareUrlNormalize?: boolean
skipTrailingSlashRedirect?: boolean
optimisticClientCache?: boolean
Expand Down
4 changes: 4 additions & 0 deletions packages/next/server/web/adapter.ts
Expand Up @@ -186,6 +186,10 @@ export async function adapter(params: {
export function blockUnallowedResponse(
promise: Promise<FetchEventResult>
): Promise<FetchEventResult> {
if (process.env.__NEXT_ALLOW_MIDDLEWARE_RESPONSE_BODY) {
return promise
}

return promise.then((result) => {
if (result.response?.body) {
console.error(
Expand Down
23 changes: 15 additions & 8 deletions packages/next/shared/lib/router/router.ts
Expand Up @@ -1187,7 +1187,7 @@ export default class Router implements BaseRouter {
// hydration. Your app should _never_ use this property. It may change at
// any time without notice.
const isQueryUpdating = (options as any)._h
const shouldResolveHref =
let shouldResolveHref =
isQueryUpdating ||
(options as any)._shouldResolveHref ||
parsePath(url).pathname === parsePath(as).pathname
Expand Down Expand Up @@ -1418,6 +1418,10 @@ export default class Router implements BaseRouter {
pathname = this.pathname
}

if (isQueryUpdating && isMiddlewareMatch) {
shouldResolveHref = false
}

if (shouldResolveHref && pathname !== '/_error') {
;(options as any)._shouldResolveHref = true

Expand Down Expand Up @@ -1955,12 +1959,14 @@ export default class Router implements BaseRouter {
isBackground: isQueryUpdating,
}

const data = await withMiddlewareEffects({
fetchData: () => fetchNextData(fetchNextDataParams),
asPath: resolvedAs,
locale: locale,
router: this,
})
const data = isQueryUpdating
? ({} as any)
: await withMiddlewareEffects({
fetchData: () => fetchNextData(fetchNextDataParams),
asPath: resolvedAs,
locale: locale,
router: this,
})

if (isQueryUpdating && data) {
data.json = self.__NEXT_DATA__.props
Expand Down Expand Up @@ -2079,7 +2085,8 @@ export default class Router implements BaseRouter {
if (
!this.isPreview &&
routeInfo.__N_SSG &&
process.env.NODE_ENV !== 'development'
process.env.NODE_ENV !== 'development' &&
!isQueryUpdating
) {
fetchNextData(
Object.assign({}, fetchNextDataParams, {
Expand Down
3 changes: 1 addition & 2 deletions test/e2e/middleware-general/test/index.test.ts
Expand Up @@ -224,14 +224,13 @@ describe('Middleware Runtime', () => {

await check(
() => browser.eval('document.documentElement.innerHTML'),
/"from":"middleware"/
/"slug":"hello"/
)

await check(() => browser.elementByCss('body').text(), /\/to-ssg/)

expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
slug: 'hello',
from: 'middleware',
})
expect(
JSON.parse(await browser.elementByCss('#props').text()).params
Expand Down
1 change: 0 additions & 1 deletion test/e2e/middleware-trailing-slash/test/index.test.ts
Expand Up @@ -121,7 +121,6 @@ describe('Middleware Runtime trailing slash', () => {

expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
slug: 'hello',
from: 'middleware',
})
expect(
JSON.parse(await browser.elementByCss('#props').text()).params
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/skip-trailing-slash-redirect/app/middleware.js
Expand Up @@ -23,5 +23,13 @@ export default function handler(req) {
return res
}

if (req.nextUrl.pathname === '/middleware-response-body') {
return new Response('hello from middleware', {
headers: {
'x-from-middleware': 'true',
},
})
}

return NextResponse.next()
}
27 changes: 27 additions & 0 deletions test/e2e/skip-trailing-slash-redirect/app/next.config.js
@@ -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
30 changes: 7 additions & 23 deletions test/e2e/skip-trailing-slash-redirect/index.test.ts
Expand Up @@ -11,33 +11,17 @@ describe('skip-trailing-slash-redirect', () => {
next = await createNext({
files: new FileRef(join(__dirname, 'app')),
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())

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 () => {
const res = await fetchViaHTTP(next.url, '/api/test-cookie', undefined, {
redirect: 'manual',
Expand Down
14 changes: 8 additions & 6 deletions test/integration/dynamic-routing/test/index.test.js
Expand Up @@ -47,11 +47,6 @@ function runTests({ dev, serverless }) {

const cacheKeys = await getCacheKeys()
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/hello1/hello2.json?rest=hello1&rest=hello2',
'/_next/data/BUILD_ID/p1/p2/nested-all-ssg/hello.json?rest=hello',
Expand Down Expand Up @@ -97,7 +92,14 @@ function runTests({ dev, serverless }) {
await browser.waitForElementByCss(linkSelector)
}
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,
])
})
}

Expand Down
14 changes: 0 additions & 14 deletions test/integration/middleware-prefetch/tests/index.test.js
Expand Up @@ -3,7 +3,6 @@
import { join } from 'path'
import fs from 'fs-extra'
import webdriver from 'next-webdriver'
import assert from 'assert'
import { check, findPort, killApp, nextBuild, nextStart } from 'next-test-utils'

jest.setTimeout(1000 * 60 * 2)
Expand Down Expand Up @@ -62,19 +61,6 @@ describe('Middleware Production Prefetch', () => {
}, '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 () => {
const browser = await webdriver(context.appPort, `/`)
await browser.elementByCss('#ssg-page-2').moveTo()
Expand Down

0 comments on commit 35308c6

Please sign in to comment.