Skip to content

Commit

Permalink
fix rewrites to edge routes (#58797)
Browse files Browse the repository at this point in the history
### What?
Rewrites to an edge route currently throw an invariant rather than properly serving up the page that is rewritten to.

### Why?
The `NextRequest` object that is provided to the edge route handler contains pathname information only for the "origin" request (e.g., when visiting `/one/example` which rewrites to `/two/example`, the pathname is still `/one/example`. This hits an invariant since the route matcher is unable to find `/one/example` since it does not exist.

### How?
This updates the module wrapper to grab the pathname from the route definition rather than the request object. For dynamic segments, we extract them from `request.nextUrl` since we know that even if `nextUrl` is referencing the origin path, the parameters it has are relevant for the rewrite. 

This adds the `getUtils` utility that's also used in base-server to handle the URL normalization, which also provides an interface for normalizing dynamic params from a `ParsedUrlQuery`.

Closes NEXT-1724
Fixes #48295
  • Loading branch information
ztanner authored Nov 23, 2023
1 parent 2f07579 commit 0f642dc
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 20 deletions.
34 changes: 14 additions & 20 deletions packages/next/src/server/web/edge-route-module-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import './globals'
import { adapter, type AdapterOptions } from './adapter'
import { IncrementalCache } from '../lib/incremental-cache'
import { RouteMatcher } from '../future/route-matchers/route-matcher'
import { removeTrailingSlash } from '../../shared/lib/router/utils/remove-trailing-slash'
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
import type { NextFetchEvent } from './spec-extension/fetch-event'
import { internal_getCurrentFunctionWaitUntil } from './internal-edge-wait-until'
import { getUtils } from '../server-utils'

type WrapOptions = Partial<Pick<AdapterOptions, 'page'>>

Expand Down Expand Up @@ -68,24 +67,19 @@ export class EdgeRouteModuleWrapper {
request: NextRequest,
evt: NextFetchEvent
): Promise<Response> {
// Get the pathname for the matcher. Pathnames should not have trailing
// slashes for matching.
let pathname = removeTrailingSlash(new URL(request.url).pathname)
const utils = getUtils({
pageIsDynamic: this.matcher.isDynamic,
page: this.matcher.definition.pathname,
basePath: request.nextUrl.basePath,
// We don't need the `handleRewrite` util, so can just pass an empty object
rewrites: {},
// only used for rewrites, so setting an arbitrary default value here
caseSensitive: false,
})

// Get the base path and strip it from the pathname if it exists.
const { basePath } = request.nextUrl
if (basePath) {
// If the path prefix doesn't exist, then this will do nothing.
pathname = removePathPrefix(pathname, basePath)
}

// Get the match for this request.
const match = this.matcher.match(pathname)
if (!match) {
throw new Error(
`Invariant: no match found for request. Pathname '${pathname}' should have matched '${this.matcher.definition.pathname}'`
)
}
const { params } = utils.normalizeDynamicRouteParams(
Object.fromEntries(request.nextUrl.searchParams)
)

const prerenderManifest: PrerenderManifest | undefined =
typeof self.__PRERENDER_MANIFEST === 'string'
Expand All @@ -95,7 +89,7 @@ export class EdgeRouteModuleWrapper {
// Create the context for the handler. This contains the params from the
// match (if any).
const context: AppRouteRouteHandlerContext = {
params: match.params,
params,
prerenderManifest: {
version: 4,
routes: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const runtime = 'edge'
export const dynamic = 'force-dynamic'

export function GET(req, { params }) {
return new Response(
`Hello from /app/dynamic/[slug]/route.ts. Slug: ${params.slug}`
)
}
16 changes: 16 additions & 0 deletions test/e2e/app-dir/edge-route-rewrite/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
6 changes: 6 additions & 0 deletions test/e2e/app-dir/edge-route-rewrite/app/two/example/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const runtime = 'edge'
export const dynamic = 'force-dynamic'

export function GET() {
return new Response('Hello from /app/two/example/route.ts')
}
21 changes: 21 additions & 0 deletions test/e2e/app-dir/edge-route-rewrite/edge-route-rewrite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createNextDescribe } from 'e2e-utils'

createNextDescribe(
'edge-route-rewrite',
{
files: __dirname,
},
({ next }) => {
it('it should support a rewrite to an edge route', async () => {
const result = await next.render('/one/example')
expect(result).toContain('Hello from /app/two/example/route.ts')
})

it('it should support a rewrite to a dynamic edge route', async () => {
const result = await next.render('/dynamic-test/foo')
expect(result).toContain(
'Hello from /app/dynamic/[slug]/route.ts. Slug: foo'
)
})
}
)
21 changes: 21 additions & 0 deletions test/e2e/app-dir/edge-route-rewrite/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
const url = request.nextUrl

let originalPathname = url.pathname

if (url.pathname.includes('/one')) {
url.pathname = '/two/example'
} else if (url.pathname.includes('/dynamic-test')) {
url.pathname = '/dynamic/foo'
}

if (url.pathname !== originalPathname) {
return NextResponse.rewrite(url)
}
}

export const config = {
matcher: ['/one/:path*', '/dynamic-test/:path*'],
}
6 changes: 6 additions & 0 deletions test/e2e/app-dir/edge-route-rewrite/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig

0 comments on commit 0f642dc

Please # to comment.