Skip to content

Commit fa41fce

Browse files
ascorbickodiakhq[bot]
andauthoredFeb 16, 2024
fix: normalise middleware data URL requests (#267)
* fix: normalise middleware data URL requests * chore: fix test * chore: fix test * fix: strip trailing slash * Changes from review * Fix slash normalizing and redirect handling --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 39ab537 commit fa41fce

File tree

7 files changed

+116
-29
lines changed

7 files changed

+116
-29
lines changed
 

‎edge-runtime/lib/response.ts

+22-19
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
addBasePath,
88
normalizeDataUrl,
99
normalizeLocalePath,
10+
normalizeTrailingSlash,
1011
relativizeURL,
11-
rewriteDataPath,
1212
} from './util.ts'
1313
import { addMiddlewareHeaders, isMiddlewareRequest, isMiddlewareResponse } from './middleware.ts'
1414
import { RequestData } from './next-request.ts'
@@ -116,10 +116,21 @@ export const buildResponse = async ({
116116
const res = new Response(result.response.body, result.response)
117117
request.headers.set('x-nf-next-middleware', 'skip')
118118

119-
const rewrite = res.headers.get('x-middleware-rewrite')
119+
let rewrite = res.headers.get('x-middleware-rewrite')
120+
let redirect = res.headers.get('location')
120121

121122
// Data requests (i.e. requests for /_next/data ) need special handling
122123
const isDataReq = request.headers.has('x-nextjs-data')
124+
// Data requests need to be normalized to the route path
125+
if (isDataReq && !redirect && !rewrite) {
126+
const requestUrl = new URL(request.url)
127+
const normalizedDataUrl = normalizeDataUrl(requestUrl.pathname)
128+
// Don't rewrite unless the URL has changed
129+
if (normalizedDataUrl !== requestUrl.pathname) {
130+
rewrite = `${normalizedDataUrl}${requestUrl.search}`
131+
logger.withFields({ rewrite_url: rewrite }).debug('Rewritten data URL')
132+
}
133+
}
123134

124135
if (rewrite) {
125136
logger.withFields({ rewrite_url: rewrite }).debug('Found middleware rewrite')
@@ -132,7 +143,6 @@ export const buildResponse = async ({
132143
}
133144

134145
const relativeUrl = relativizeURL(rewrite, request.url)
135-
const originalPath = new URL(request.url, `http://n`).pathname
136146

137147
if (isDataReq) {
138148
// Data requests might be rewritten to an external URL
@@ -165,31 +175,24 @@ export const buildResponse = async ({
165175
}
166176

167177
return addMiddlewareHeaders(fetch(proxyRequest, { redirect: 'manual' }), res)
168-
} else if (isDataReq) {
169-
rewriteUrl.pathname = rewriteDataPath({
170-
dataUrl: originalPath,
171-
newRoute: rewriteUrl.pathname,
172-
basePath: nextConfig?.basePath,
173-
})
174-
if (rewriteUrl.toString() === request.url) {
175-
logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url')
176-
return
177-
}
178-
res.headers.set('x-middleware-rewrite', relativeUrl)
179-
return addMiddlewareHeaders(fetch(new Request(rewriteUrl, request)), res)
180178
}
181-
const target = normalizeLocalizedTarget({ target: rewrite, request, nextConfig })
179+
180+
if (isDataReq) {
181+
// The rewrite target is a data request, but a middleware rewrite target is always for the page route,
182+
// so we need to tell the server this is a data request. Setting the `x-nextjs-data` header is not enough. 🤷
183+
rewriteUrl.searchParams.set('__nextDataReq', '1')
184+
rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, nextConfig?.trailingSlash)
185+
}
186+
const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, nextConfig })
182187
if (target === request.url) {
183188
logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url')
184189
return
185190
}
186191
res.headers.set('x-middleware-rewrite', relativeUrl)
187192
request.headers.set('x-middleware-rewrite', target)
188-
return addMiddlewareHeaders(fetch(new Request(target, request)), res)
193+
return addMiddlewareHeaders(fetch(new Request(target, request), { redirect: 'manual' }), res)
189194
}
190195

191-
let redirect = res.headers.get('location')
192-
193196
// If we are redirecting a request that had a locale in the URL, we need to add it back in
194197
if (redirect && requestLocale) {
195198
redirect = normalizeLocalizedTarget({ target: redirect, request, nextConfig })

‎edge-runtime/lib/util.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,14 @@ export function relativizeURL(url: string | string, base: string | URL) {
8181

8282
export const normalizeIndex = (path: string) => (path === '/' ? '/index' : path)
8383

84-
const stripTrailingSlash = (path: string) =>
84+
export const normalizeTrailingSlash = (path: string, trailingSlash?: boolean) =>
85+
trailingSlash ? addTrailingSlash(path) : stripTrailingSlash(path)
86+
87+
export const stripTrailingSlash = (path: string) =>
8588
path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path
8689

90+
export const addTrailingSlash = (path: string) => (path.endsWith('/') ? path : `${path}/`)
91+
8792
/**
8893
* Modify a data url to point to a new page route.
8994
*/

‎src/run/headers.test.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('headers', () => {
3939

4040
expect(headers.set).toBeCalledWith(
4141
'netlify-vary',
42-
'cookie=__prerender_bypass|__next_preview_data',
42+
'header=x-nextjs-data,cookie=__prerender_bypass|__next_preview_data',
4343
)
4444
})
4545

@@ -55,7 +55,7 @@ describe('headers', () => {
5555

5656
expect(headers.set).toBeCalledWith(
5757
'netlify-vary',
58-
'header=Accept|Accept-Language,cookie=__prerender_bypass|__next_preview_data',
58+
'header=x-nextjs-data|Accept|Accept-Language,cookie=__prerender_bypass|__next_preview_data',
5959
)
6060
})
6161

@@ -76,7 +76,7 @@ describe('headers', () => {
7676

7777
expect(headers.set).toBeCalledWith(
7878
'netlify-vary',
79-
'cookie=__prerender_bypass|__next_preview_data',
79+
'header=x-nextjs-data,cookie=__prerender_bypass|__next_preview_data',
8080
)
8181
})
8282

@@ -96,7 +96,7 @@ describe('headers', () => {
9696

9797
expect(headers.set).toBeCalledWith(
9898
'netlify-vary',
99-
'cookie=__prerender_bypass|__next_preview_data',
99+
'header=x-nextjs-data,cookie=__prerender_bypass|__next_preview_data',
100100
)
101101
})
102102

@@ -116,7 +116,7 @@ describe('headers', () => {
116116

117117
expect(headers.set).toBeCalledWith(
118118
'netlify-vary',
119-
'language=en|de|fr,cookie=__prerender_bypass|__next_preview_data|NEXT_LOCALE',
119+
'header=x-nextjs-data,language=en|de|fr,cookie=__prerender_bypass|__next_preview_data|NEXT_LOCALE',
120120
)
121121
})
122122

@@ -137,7 +137,7 @@ describe('headers', () => {
137137

138138
expect(headers.set).toBeCalledWith(
139139
'netlify-vary',
140-
'language=en|de|fr,cookie=__prerender_bypass|__next_preview_data|NEXT_LOCALE',
140+
'header=x-nextjs-data,language=en|de|fr,cookie=__prerender_bypass|__next_preview_data|NEXT_LOCALE',
141141
)
142142
})
143143
})

‎src/run/headers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const setVaryHeaders = (
5555
{ basePath, i18n }: Pick<NextConfigComplete, 'basePath' | 'i18n'>,
5656
) => {
5757
const netlifyVaryValues: NetlifyVaryValues = {
58-
headers: [],
58+
headers: ['x-nextjs-data'],
5959
languages: [],
6060
cookies: ['__prerender_bypass', '__next_preview_data'],
6161
}

‎tests/fixtures/page-router/next.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const nextConfig = {
44
eslint: {
55
ignoreDuringBuilds: true,
66
},
7+
generateBuildId: () => 'build-id',
78
}
89

910
module.exports = nextConfig

‎tests/integration/edge-handler.test.ts

+64-2
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ describe('should run middleware on data requests', () => {
304304
})
305305

306306
describe('page router', () => {
307-
test<FixtureTestContext>('should rewrite data requests', async (ctx) => {
307+
test<FixtureTestContext>('middleware should rewrite data requests', async (ctx) => {
308308
await createFixture('middleware-pages', ctx)
309309
await runPlugin(ctx)
310310
const origin = await LocalServer.run(async (req, res) => {
@@ -327,8 +327,70 @@ describe('page router', () => {
327327
})
328328
const res = await response.json()
329329
const url = new URL(res.url, 'http://n/')
330-
expect(url.pathname).toBe('/_next/data/build-id/ssr-page-2.json')
330+
expect(url.pathname).toBe('/ssr-page-2/')
331+
expect(url.searchParams.get('__nextDataReq')).toBe('1')
332+
expect(res.headers['x-nextjs-data']).toBe('1')
331333
expect(response.headers.get('x-nextjs-rewrite')).toBe('/ssr-page-2/')
332334
expect(response.status).toBe(200)
333335
})
336+
337+
test<FixtureTestContext>('should rewrite un-rewritten data requests to page route', async (ctx) => {
338+
await createFixture('middleware-pages', ctx)
339+
await runPlugin(ctx)
340+
const origin = await LocalServer.run(async (req, res) => {
341+
res.write(
342+
JSON.stringify({
343+
url: req.url,
344+
headers: req.headers,
345+
}),
346+
)
347+
res.end()
348+
})
349+
ctx.cleanup?.push(() => origin.stop())
350+
const response = await invokeEdgeFunction(ctx, {
351+
functions: ['___netlify-edge-handler-middleware'],
352+
headers: {
353+
'x-nextjs-data': '1',
354+
},
355+
origin,
356+
url: `/_next/data/build-id/ssg/hello.json`,
357+
})
358+
const res = await response.json()
359+
const url = new URL(res.url, 'http://n/')
360+
expect(url.pathname).toBe('/ssg/hello/')
361+
expect(url.searchParams.get('__nextDataReq')).toBe('1')
362+
expect(res.headers['x-nextjs-data']).toBe('1')
363+
expect(response.status).toBe(200)
364+
})
365+
366+
test<FixtureTestContext>('should preserve query params in rewritten data requests', async (ctx) => {
367+
await createFixture('middleware-pages', ctx)
368+
await runPlugin(ctx)
369+
const origin = await LocalServer.run(async (req, res) => {
370+
res.write(
371+
JSON.stringify({
372+
url: req.url,
373+
headers: req.headers,
374+
}),
375+
)
376+
res.end()
377+
})
378+
ctx.cleanup?.push(() => origin.stop())
379+
const response = await invokeEdgeFunction(ctx, {
380+
functions: ['___netlify-edge-handler-middleware'],
381+
headers: {
382+
'x-nextjs-data': '1',
383+
},
384+
origin,
385+
url: `/_next/data/build-id/blog/first.json?slug=first`,
386+
})
387+
const res = await response.json()
388+
console.log(res)
389+
const url = new URL(res.url, 'http://n/')
390+
expect(url.pathname).toBe('/blog/first/')
391+
expect(url.searchParams.get('__nextDataReq')).toBe('1')
392+
expect(url.searchParams.get('slug')).toBe('first')
393+
expect(res.headers['x-nextjs-data']).toBe('1')
394+
expect(response.status).toBe(200)
395+
})
334396
})

‎tests/integration/page-router.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,19 @@ test<FixtureTestContext>('Should revalidate path with On-demand Revalidation', a
9797
console.log({ dateCacheInitial, dateCacheRevalidated })
9898
expect(dateCacheInitial).not.toBe(dateCacheRevalidated)
9999
})
100+
101+
test<FixtureTestContext>('Should return JSON for data req to page route', async (ctx) => {
102+
await createFixture('page-router', ctx)
103+
await runPlugin(ctx)
104+
105+
const response = await invokeFunction(ctx, {
106+
url: '/static/revalidate-manual?__nextDataReq=1',
107+
headers: { 'x-nextjs-data': '1' },
108+
})
109+
110+
expect(response.body).toMatch(/^{"pageProps":/)
111+
112+
const data = JSON.parse(response.body)
113+
114+
expect(data.pageProps.show).toBeDefined()
115+
})

0 commit comments

Comments
 (0)
Please sign in to comment.