diff --git a/packages/next/src/server/web/edge-route-module-wrapper.ts b/packages/next/src/server/web/edge-route-module-wrapper.ts index 027f49ea2d514..2b19616fb2f1a 100644 --- a/packages/next/src/server/web/edge-route-module-wrapper.ts +++ b/packages/next/src/server/web/edge-route-module-wrapper.ts @@ -4,11 +4,12 @@ import type { RouteModule } from '../future/route-modules/route-module' import type { NextRequest } from './spec-extension/request' import './globals' + import { adapter, type AdapterOptions } from './adapter' import { IncrementalCache } from '../lib/incremental-cache' - -import { removeTrailingSlash } from '../../shared/lib/router/utils/remove-trailing-slash' 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' type WrapOptions = Partial> @@ -64,7 +65,14 @@ export class EdgeRouteModuleWrapper { private async handler(request: NextRequest): Promise { // Get the pathname for the matcher. Pathnames should not have trailing // slashes for matching. - const pathname = removeTrailingSlash(new URL(request.url).pathname) + let pathname = removeTrailingSlash(new URL(request.url).pathname) + + // 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) diff --git a/test/e2e/app-dir/app-routes/app-custom-route-base-path.test.ts b/test/e2e/app-dir/app-routes/app-custom-route-base-path.test.ts new file mode 100644 index 0000000000000..07eee0a60137e --- /dev/null +++ b/test/e2e/app-dir/app-routes/app-custom-route-base-path.test.ts @@ -0,0 +1,2 @@ +process.env.BASE_PATH = '/docs' +require('./app-custom-routes.test') diff --git a/test/e2e/app-dir/app-routes/app-custom-routes.test.ts b/test/e2e/app-dir/app-routes/app-custom-routes.test.ts index 12dfdcf5e06f3..56b627dcb332c 100644 --- a/test/e2e/app-dir/app-routes/app-custom-routes.test.ts +++ b/test/e2e/app-dir/app-routes/app-custom-routes.test.ts @@ -8,6 +8,8 @@ import { cookieWithRequestMeta, } from './helpers' +const bathPath = process.env.BASE_PATH ?? '' + createNextDescribe( 'app-custom-routes', { @@ -24,7 +26,9 @@ createNextDescribe( await next.readFile('.next/server/app/api/hello.json.meta') ).toBeTruthy() } - expect(JSON.parse(await next.render('/api/hello.json'))).toEqual({ + expect( + JSON.parse(await next.render(bathPath + '/api/hello.json')) + ).toEqual({ pathname: '/api/hello.json', }) }) @@ -42,7 +46,9 @@ createNextDescribe( .catch(() => '') ).toBeFalsy() } - expect(JSON.parse(await next.render('/api/dynamic'))).toEqual({ + expect( + JSON.parse(await next.render(bathPath + '/api/dynamic')) + ).toEqual({ pathname: '/api/dynamic', query: {}, }) @@ -55,7 +61,7 @@ createNextDescribe( '/static/second/data.json', '/static/three/data.json', ])('responds correctly on %s', async (path) => { - expect(JSON.parse(await next.render(path))).toEqual({ + expect(JSON.parse(await next.render(bathPath + path))).toEqual({ params: { slug: path.split('/')[2] }, now: expect.any(Number), }) @@ -77,14 +83,16 @@ createNextDescribe( '/revalidate-1/second/data.json', '/revalidate-1/three/data.json', ])('revalidates correctly on %s', async (path) => { - const data = JSON.parse(await next.render(path)) + const data = JSON.parse(await next.render(bathPath + path)) expect(data).toEqual({ params: { slug: path.split('/')[2] }, now: expect.any(Number), }) await check(async () => { - expect(data).not.toEqual(JSON.parse(await next.render(path))) + expect(data).not.toEqual( + JSON.parse(await next.render(bathPath + path)) + ) return 'success' }, 'success') @@ -109,7 +117,7 @@ createNextDescribe( it.each(['/basic/endpoint', '/basic/vercel/endpoint'])( 'responds correctly on %s', async (path) => { - const res = await next.fetch(path, { method }) + const res = await next.fetch(bathPath + path, { method }) expect(res.status).toEqual(200) expect(await res.text()).toContain('hello, world') @@ -123,7 +131,7 @@ createNextDescribe( describe('route groups', () => { it('routes to the correct handler', async () => { - const res = await next.fetch('/basic/endpoint/nested') + const res = await next.fetch(bathPath + '/basic/endpoint/nested') expect(res.status).toEqual(200) const meta = getRequestMeta(res.headers) @@ -133,7 +141,7 @@ createNextDescribe( describe('request', () => { it('can read query parameters', async () => { - const res = await next.fetch('/advanced/query?ping=pong') + const res = await next.fetch(bathPath + '/advanced/query?ping=pong') expect(res.status).toEqual(200) const meta = getRequestMeta(res.headers) @@ -141,7 +149,9 @@ createNextDescribe( }) it('can read query parameters (edge)', async () => { - const res = await next.fetch('/edge/advanced/query?ping=pong') + const res = await next.fetch( + bathPath + '/edge/advanced/query?ping=pong' + ) expect(res.status).toEqual(200) const meta = getRequestMeta(res.headers) @@ -152,7 +162,7 @@ createNextDescribe( describe('response', () => { // TODO-APP: re-enable when rewrites are supported again it.skip('supports the NextResponse.rewrite() helper', async () => { - const res = await next.fetch('/hooks/rewrite') + const res = await next.fetch(bathPath + '/hooks/rewrite') expect(res.status).toEqual(200) @@ -163,7 +173,7 @@ createNextDescribe( }) it('supports the NextResponse.redirect() helper', async () => { - const res = await next.fetch('/hooks/redirect/response', { + const res = await next.fetch(bathPath + '/hooks/redirect/response', { // "Manually" perform the redirect, we want to inspect the // redirection response, so don't actually follow it. redirect: 'manual', @@ -176,7 +186,7 @@ createNextDescribe( it('supports the NextResponse.json() helper', async () => { const meta = { ping: 'pong' } - const res = await next.fetch('/hooks/json', { + const res = await next.fetch(bathPath + '/hooks/json', { headers: withRequestMeta(meta), }) @@ -202,7 +212,7 @@ createNextDescribe( }, }) - const res = await next.fetch('/advanced/body/streaming', { + const res = await next.fetch(bathPath + '/advanced/body/streaming', { method: 'POST', body: stream, }) @@ -224,10 +234,13 @@ createNextDescribe( }, }) - const res = await next.fetch('/edge/advanced/body/streaming', { - method: 'POST', - body: stream, - }) + const res = await next.fetch( + bathPath + '/edge/advanced/body/streaming', + { + method: 'POST', + body: stream, + } + ) expect(res.status).toEqual(200) expect(await res.text()).toEqual(body.join('\n') + '\n') @@ -235,7 +248,7 @@ createNextDescribe( it('can read a JSON encoded body', async () => { const body = { ping: 'pong' } - const res = await next.fetch('/advanced/body/json', { + const res = await next.fetch(bathPath + '/advanced/body/json', { method: 'POST', body: JSON.stringify(body), }) @@ -247,7 +260,7 @@ createNextDescribe( it('can read a JSON encoded body (edge)', async () => { const body = { ping: 'pong' } - const res = await next.fetch('/edge/advanced/body/json', { + const res = await next.fetch(bathPath + '/edge/advanced/body/json', { method: 'POST', body: JSON.stringify(body), }) @@ -259,7 +272,7 @@ createNextDescribe( it('can read a JSON encoded body for DELETE requests', async () => { const body = { name: 'foo' } - const res = await next.fetch('/advanced/body/json', { + const res = await next.fetch(bathPath + '/advanced/body/json', { method: 'DELETE', body: JSON.stringify(body), }) @@ -270,7 +283,7 @@ createNextDescribe( it('can read a JSON encoded body for OPTIONS requests', async () => { const body = { name: 'bar' } - const res = await next.fetch('/advanced/body/json', { + const res = await next.fetch(bathPath + '/advanced/body/json', { method: 'OPTIONS', body: JSON.stringify(body), }) @@ -293,7 +306,7 @@ createNextDescribe( index++ }, }) - const res = await next.fetch('/advanced/body/json', { + const res = await next.fetch(bathPath + '/advanced/body/json', { method: 'POST', body: stream, }) @@ -316,7 +329,7 @@ createNextDescribe( index++ }, }) - const res = await next.fetch('/edge/advanced/body/json', { + const res = await next.fetch(bathPath + '/edge/advanced/body/json', { method: 'POST', body: stream, }) @@ -328,7 +341,7 @@ createNextDescribe( it('can read the text body', async () => { const body = 'hello, world' - const res = await next.fetch('/advanced/body/text', { + const res = await next.fetch(bathPath + '/advanced/body/text', { method: 'POST', body, }) @@ -340,7 +353,7 @@ createNextDescribe( it('can read the text body (edge)', async () => { const body = 'hello, world' - const res = await next.fetch('/edge/advanced/body/text', { + const res = await next.fetch(bathPath + '/edge/advanced/body/text', { method: 'POST', body, }) @@ -353,7 +366,7 @@ createNextDescribe( describe('context', () => { it('provides params to routes with dynamic parameters', async () => { - const res = await next.fetch('/basic/vercel/endpoint') + const res = await next.fetch(bathPath + '/basic/vercel/endpoint') expect(res.status).toEqual(200) const meta = getRequestMeta(res.headers) @@ -361,7 +374,9 @@ createNextDescribe( }) it('provides params to routes with catch-all routes', async () => { - const res = await next.fetch('/basic/vercel/some/other/resource') + const res = await next.fetch( + bathPath + '/basic/vercel/some/other/resource' + ) expect(res.status).toEqual(200) const meta = getRequestMeta(res.headers) @@ -372,7 +387,7 @@ createNextDescribe( }) it('does not provide params to routes without dynamic parameters', async () => { - const res = await next.fetch('/basic/endpoint') + const res = await next.fetch(bathPath + '/basic/endpoint') expect(res.ok).toBeTrue() @@ -384,7 +399,7 @@ createNextDescribe( describe('hooks', () => { describe('headers', () => { it('gets the correct values', async () => { - const res = await next.fetch('/hooks/headers', { + const res = await next.fetch(bathPath + '/hooks/headers', { headers: withRequestMeta({ ping: 'pong' }), }) @@ -397,7 +412,7 @@ createNextDescribe( describe('cookies', () => { it('gets the correct values', async () => { - const res = await next.fetch('/hooks/cookies', { + const res = await next.fetch(bathPath + '/hooks/cookies', { headers: cookieWithRequestMeta({ ping: 'pong' }), }) @@ -410,7 +425,7 @@ createNextDescribe( describe('redirect', () => { it('can respond correctly', async () => { - const res = await next.fetch('/hooks/redirect', { + const res = await next.fetch(bathPath + '/hooks/redirect', { // "Manually" perform the redirect, we want to inspect the // redirection response, so don't actually follow it. redirect: 'manual', @@ -424,7 +439,7 @@ createNextDescribe( describe('notFound', () => { it('can respond correctly', async () => { - const res = await next.fetch('/hooks/not-found') + const res = await next.fetch(bathPath + '/hooks/not-found') expect(res.status).toEqual(404) expect(await res.text()).toBeEmpty() @@ -434,21 +449,25 @@ createNextDescribe( describe('error conditions', () => { it('responds with 400 (Bad Request) when the requested method is not a valid HTTP method', async () => { - const res = await next.fetch('/status/405', { method: 'HEADER' }) + const res = await next.fetch(bathPath + '/status/405', { + method: 'HEADER', + }) expect(res.status).toEqual(400) expect(await res.text()).toBeEmpty() }) it('responds with 405 (Method Not Allowed) when method is not implemented', async () => { - const res = await next.fetch('/status/405', { method: 'POST' }) + const res = await next.fetch(bathPath + '/status/405', { + method: 'POST', + }) expect(res.status).toEqual(405) expect(await res.text()).toBeEmpty() }) it('responds with 500 (Internal Server Error) when the handler throws an error', async () => { - const res = await next.fetch('/status/500') + const res = await next.fetch(bathPath + '/status/500') expect(res.status).toEqual(500) expect(await res.text()).toBeEmpty() @@ -462,7 +481,7 @@ createNextDescribe( // testing that the specific route throws this error in the console. expect(next.cliOutput).not.toContain(error) - const res = await next.fetch('/status/500/next') + const res = await next.fetch(bathPath + '/status/500/next') expect(res.status).toEqual(500) expect(await res.text()).toBeEmpty() @@ -478,14 +497,18 @@ createNextDescribe( describe('automatic implementations', () => { it('implements HEAD on routes with GET already implemented', async () => { - const res = await next.fetch('/methods/head', { method: 'HEAD' }) + const res = await next.fetch(bathPath + '/methods/head', { + method: 'HEAD', + }) expect(res.status).toEqual(200) expect(await res.text()).toBeEmpty() }) it('implements OPTIONS on routes', async () => { - const res = await next.fetch('/methods/options', { method: 'OPTIONS' }) + const res = await next.fetch(bathPath + '/methods/options', { + method: 'OPTIONS', + }) expect(res.status).toEqual(204) expect(await res.text()).toBeEmpty() @@ -498,7 +521,7 @@ createNextDescribe( describe('edge functions', () => { it('returns response using edge runtime', async () => { - const res = await next.fetch('/edge') + const res = await next.fetch(bathPath + '/edge') expect(res.status).toEqual(200) expect(await res.text()).toContain('hello, world') @@ -506,7 +529,7 @@ createNextDescribe( it('returns a response when headers are accessed', async () => { const meta = { ping: 'pong' } - const res = await next.fetch('/edge/headers', { + const res = await next.fetch(bathPath + '/edge/headers', { headers: withRequestMeta(meta), }) @@ -517,7 +540,7 @@ createNextDescribe( describe('dynamic = "force-static"', () => { it('strips search, headers, and domain from request', async () => { - const res = await next.fetch('/dynamic?query=true', { + const res = await next.fetch(bathPath + '/dynamic?query=true', { headers: { accept: 'application/json', cookie: 'session=true', @@ -546,7 +569,7 @@ createNextDescribe( describe('customized metadata routes', () => { it('should work if conflict with metadata routes convention', async () => { - const res = await next.fetch('/robots.txt') + const res = await next.fetch(bathPath + '/robots.txt') expect(res.status).toEqual(200) expect(await res.text()).toBe( @@ -568,7 +591,7 @@ createNextDescribe( ])( 'should print an error when using lowercase %p in dev', async (method: string) => { - await next.fetch('/lowercase/' + method) + await next.fetch(bathPath + '/lowercase/' + method) await check(() => { expect(next.cliOutput).toContain( @@ -588,7 +611,7 @@ createNextDescribe( describe('invalid exports', () => { it('should print an error when exporting a default handler in dev', async () => { - const res = await next.fetch('/default') + const res = await next.fetch(bathPath + '/default') // Ensure we get a 405 (Method Not Allowed) response when there is no // exported handler for the GET method. diff --git a/test/e2e/app-dir/app-routes/next.config.js b/test/e2e/app-dir/app-routes/next.config.js index 79d95000de310..7670c4ad0ebb5 100644 --- a/test/e2e/app-dir/app-routes/next.config.js +++ b/test/e2e/app-dir/app-routes/next.config.js @@ -1,8 +1,14 @@ /** * @type {import('next').NextConfig} */ -module.exports = { +const config = { typescript: { ignoreBuildErrors: true, }, } + +if (process.env.BASE_PATH) { + config.basePath = process.env.BASE_PATH +} + +module.exports = config