@@ -109,6 +109,35 @@ type StartedTextSequenceTarget = {
109109 url : string ;
110110} ;
111111
112+ async function startPathEchoTarget ( ) : Promise < StartedTextSequenceTarget > {
113+ const upstream = http . createServer ( ( req , res ) => {
114+ res . setHeader ( "content-type" , "application/json" ) ;
115+ res . end ( JSON . stringify ( { pathname : new URL ( req . url ?? "/" , "http://localhost" ) . pathname } ) ) ;
116+ } ) ;
117+
118+ await new Promise < void > ( ( resolve , reject ) => {
119+ upstream . once ( "error" , reject ) ;
120+ upstream . listen ( 0 , "127.0.0.1" , ( ) => {
121+ upstream . off ( "error" , reject ) ;
122+ resolve ( ) ;
123+ } ) ;
124+ } ) ;
125+
126+ const address = upstream . address ( ) ;
127+ if ( ! address || typeof address === "string" ) {
128+ throw new Error ( "Path echo target did not bind to a TCP port" ) ;
129+ }
130+
131+ return {
132+ url : `http://127.0.0.1:${ address . port } ` ,
133+ close ( ) {
134+ return new Promise < void > ( ( resolve , reject ) => {
135+ upstream . close ( ( error ) => ( error ? reject ( error ) : resolve ( ) ) ) ;
136+ } ) ;
137+ } ,
138+ } ;
139+ }
140+
112141async function startTextSequenceTarget ( options ?: {
113142 prefix ?: string ;
114143 recordRequest ?: ( req : http . IncomingMessage ) => void ;
@@ -212,6 +241,7 @@ describe("App Router Production server (startProdServer)", () => {
212241 let appStaticDynamicTarget : StartedTextSequenceTarget | undefined ;
213242 let appStaticRevalidateTarget : StartedTextSequenceTarget | undefined ;
214243 let revalidatePathFetchTarget : StartedTextSequenceTarget | undefined ;
244+ let externalRewriteTarget : StartedTextSequenceTarget | undefined ;
215245
216246 function extractRequestId ( html : string ) : string | undefined {
217247 return (
@@ -257,6 +287,7 @@ describe("App Router Production server (startProdServer)", () => {
257287 }
258288
259289 beforeAll ( async ( ) => {
290+ externalRewriteTarget = await startPathEchoTarget ( ) ;
260291 appStaticDelayTarget = await startDelayedJsonSequenceTarget ( 750 ) ;
261292 appStaticDynamicTarget = await startTextSequenceTarget ( { prefix : "dynamic" } ) ;
262293 appStaticRevalidateTarget = await startTextSequenceTarget ( { prefix : "revalidate" } ) ;
@@ -265,6 +296,7 @@ describe("App Router Production server (startProdServer)", () => {
265296 process . env . TEST_APP_STATIC_DYNAMIC_TARGET = appStaticDynamicTarget . url ;
266297 process . env . TEST_APP_STATIC_REVALIDATE_TARGET = appStaticRevalidateTarget . url ;
267298 process . env . TEST_REVALIDATE_PATH_REWRITES_TARGET = revalidatePathFetchTarget . url ;
299+ process . env . TEST_EXTERNAL_REWRITE_ORIGIN = externalRewriteTarget . url ;
268300
269301 try {
270302 // Build the app-basic fixture to the default dist/ directory
@@ -289,15 +321,18 @@ describe("App Router Production server (startProdServer)", () => {
289321 await appStaticDynamicTarget ?. close ( ) ;
290322 await appStaticRevalidateTarget ?. close ( ) ;
291323 await revalidatePathFetchTarget ?. close ( ) ;
324+ await externalRewriteTarget ?. close ( ) ;
292325 } finally {
293326 appStaticDelayTarget = undefined ;
294327 appStaticDynamicTarget = undefined ;
295328 appStaticRevalidateTarget = undefined ;
296329 revalidatePathFetchTarget = undefined ;
330+ externalRewriteTarget = undefined ;
297331 delete process . env . TEST_APP_STATIC_DELAY_TARGET ;
298332 delete process . env . TEST_APP_STATIC_DYNAMIC_TARGET ;
299333 delete process . env . TEST_APP_STATIC_REVALIDATE_TARGET ;
300334 delete process . env . TEST_REVALIDATE_PATH_REWRITES_TARGET ;
335+ delete process . env . TEST_EXTERNAL_REWRITE_ORIGIN ;
301336 }
302337 throw error ;
303338 }
@@ -309,10 +344,12 @@ describe("App Router Production server (startProdServer)", () => {
309344 await appStaticDynamicTarget ?. close ( ) ;
310345 await appStaticRevalidateTarget ?. close ( ) ;
311346 await revalidatePathFetchTarget ?. close ( ) ;
347+ await externalRewriteTarget ?. close ( ) ;
312348 delete process . env . TEST_APP_STATIC_DELAY_TARGET ;
313349 delete process . env . TEST_APP_STATIC_DYNAMIC_TARGET ;
314350 delete process . env . TEST_APP_STATIC_REVALIDATE_TARGET ;
315351 delete process . env . TEST_REVALIDATE_PATH_REWRITES_TARGET ;
352+ delete process . env . TEST_EXTERNAL_REWRITE_ORIGIN ;
316353 fs . rmSync ( outDir , { recursive : true , force : true } ) ;
317354 } ) ;
318355
@@ -325,6 +362,16 @@ describe("App Router Production server (startProdServer)", () => {
325362 expect ( html ) . toContain ( "<script" ) ;
326363 } ) ;
327364
365+ it ( "preserves external rewrite path prefixes" , async ( ) => {
366+ for ( const pathname of [ "%252e%252e" , ".%252e" , "%252e." ] ) {
367+ const response = await fetch ( `${ baseUrl } /external-prefix/${ pathname } /outside` ) ;
368+ expect ( response . status ) . toBe ( 200 ) ;
369+ expect ( await response . json ( ) ) . toEqual ( {
370+ pathname : expect . stringMatching ( / ^ \/ v 1 (?: \/ | $ ) / ) ,
371+ } ) ;
372+ }
373+ } ) ;
374+
328375 // Ported from Next.js: test/e2e/app-dir/app-static/app-static.test.ts
329376 // https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/app-static/app-static.test.ts
330377 it ( "contains dynamic = 'error' failures without terminating later App Router requests" , async ( ) => {
0 commit comments