diff --git a/cypress/e2e/middleware/standard.cy.ts b/cypress/e2e/middleware/standard.cy.ts index 780f9f0c85..4992e0186c 100644 --- a/cypress/e2e/middleware/standard.cy.ts +++ b/cypress/e2e/middleware/standard.cy.ts @@ -45,6 +45,12 @@ describe('Standard middleware', () => { expect(response.headers).to.have.property('x-foo', 'bar') }) }) + + it('preserves locale on rewrites (skipMiddlewareUrlNormalize: true)', () => { + cy.visit('/de-de/locale-preserving-rewrite') + cy.get('div').should('contain', 'Locale: de-DE') + cy.url().should('eq', `${Cypress.config().baseUrl}/de-de/locale-preserving-rewrite`) + }) }) describe('Middleware matchers', () => { diff --git a/demos/middleware/middleware.ts b/demos/middleware/middleware.ts index d93e5fa9b6..9206540da1 100644 --- a/demos/middleware/middleware.ts +++ b/demos/middleware/middleware.ts @@ -21,7 +21,9 @@ export async function middleware(req: NextRequest) { } const request = new MiddlewareRequest(req) - if (pathname.startsWith('/static')) { + + // skipMiddlewareUrlNormalize next config option is used so we have to try to match both html path and data blob path + if (pathname.startsWith('/static') || pathname.endsWith('/static.json')) { // Unlike NextResponse.next(), this actually sends the request to the origin const res = await request.next() const message = `This was static (& escaping test &) but has been transformed in ${req.geo?.city}` @@ -36,7 +38,8 @@ export async function middleware(req: NextRequest) { return res } - if (pathname.startsWith('/request-rewrite')) { + // skipMiddlewareUrlNormalize next config option is used so we have to try to match both html path and data blob path + if (pathname.startsWith('/request-rewrite') || pathname.endsWith('/request-rewrite.json')) { // request.rewrite() should return the MiddlewareResponse object instead of the Response object. const res = await request.rewrite('/static-rewrite') const message = `This was static (& escaping test &) but has been transformed in ${req.geo?.city}` @@ -100,6 +103,10 @@ export async function middleware(req: NextRequest) { return response } + if (pathname.includes('locale-preserving-rewrite')) { + return NextResponse.rewrite(new URL('/locale-test', req.url)) + } + if (pathname.startsWith('/shows')) { if (pathname.startsWith('/shows/222')) { response = NextResponse.next() @@ -151,6 +158,7 @@ export const config = { matcher: [ '/api/:all*', '/headers', + '/:all*/locale-preserving-rewrite', '/cookies/:path*', { source: '/static' }, {source: '/request-rewrite' }, diff --git a/demos/middleware/next.config.js b/demos/middleware/next.config.js index 9ce274ced3..ee7bb8a36e 100644 --- a/demos/middleware/next.config.js +++ b/demos/middleware/next.config.js @@ -11,6 +11,7 @@ const nextConfig = { defaultLocale: 'en', locales: ['en', 'de-DE'], }, + skipMiddlewareUrlNormalize: true, } module.exports = nextConfig diff --git a/demos/middleware/pages/locale-test.js b/demos/middleware/pages/locale-test.js new file mode 100644 index 0000000000..67d8592b1c --- /dev/null +++ b/demos/middleware/pages/locale-test.js @@ -0,0 +1,15 @@ +import * as React from 'react' + +const Page = ({ pageLocale }) => { + return
Locale: {pageLocale}
+} + +export async function getServerSideProps({ locale }) { + return { + props: { + pageLocale: locale, + }, + } +} + +export default Page diff --git a/packages/runtime/src/templates/edge-shared/utils.ts b/packages/runtime/src/templates/edge-shared/utils.ts index 09ea64a113..f679ad2037 100644 --- a/packages/runtime/src/templates/edge-shared/utils.ts +++ b/packages/runtime/src/templates/edge-shared/utils.ts @@ -270,6 +270,9 @@ export const buildResponse = async ({ } res.headers.set('x-middleware-rewrite', relativeUrl) + request.headers.set('x-original-path', new URL(request.url, `http://n`).pathname) + request.headers.set('x-middleware-rewrite', rewrite) + return addMiddlewareHeaders(context.rewrite(rewrite), res) } diff --git a/packages/runtime/src/templates/handlerUtils.ts b/packages/runtime/src/templates/handlerUtils.ts index dce9ea7541..01ae1b1024 100644 --- a/packages/runtime/src/templates/handlerUtils.ts +++ b/packages/runtime/src/templates/handlerUtils.ts @@ -189,6 +189,13 @@ export const normalizePath = (event: HandlerEvent) => { return originalPath } } + + if (event.headers['x-original-path']) { + if (event.headers['x-next-debug-logging']) { + console.log('Original path:', event.headers['x-original-path']) + } + return event.headers['x-original-path'] + } // Ensure that paths are encoded - but don't double-encode them return new URL(event.rawUrl).pathname } diff --git a/packages/runtime/src/templates/server.ts b/packages/runtime/src/templates/server.ts index 577d6d7b0d..0c8d86f486 100644 --- a/packages/runtime/src/templates/server.ts +++ b/packages/runtime/src/templates/server.ts @@ -1,4 +1,7 @@ -import { PrerenderManifest } from 'next/dist/build' +// eslint-disable-next-line n/no-deprecated-api -- this is what Next.js uses as well +import { parse } from 'url' + +import type { PrerenderManifest } from 'next/dist/build' import type { BaseNextResponse } from 'next/dist/server/base-http' import type { NodeRequestHandler, Options } from 'next/dist/server/next-server' @@ -36,6 +39,10 @@ const getNetlifyNextServer = (NextServer: NextServerType) => { public getRequestHandler(): NodeRequestHandler { const handler = super.getRequestHandler() return async (req, res, parsedUrl) => { + if (!parsedUrl && typeof req?.headers?.['x-middleware-rewrite'] === 'string') { + parsedUrl = parse(req.headers['x-middleware-rewrite'], true) + } + // preserve the URL before Next.js mutates it for i18n const { url, headers } = req