@@ -19,15 +19,26 @@ describe('ISR', () => {
1919 assert . deepEqual ( vcConfig , {
2020 expiration : 120 ,
2121 bypassToken : '1c9e601d-9943-4e7c-9575-005556d774a8' ,
22- allowQuery : [ 'x_astro_path' ] ,
22+ allowQuery : [ 'x_astro_path' , 'x_astro_path_token' ] ,
2323 passQuery : true ,
2424 } ) ;
2525 } ) ;
2626
2727 it ( 'generates expected routes' , { timeout : 30000 } , async ( ) => {
2828 const deploymentConfig = JSON . parse ( await fixture . readFile ( '../.vercel/output/config.json' ) ) ;
29+ // The path token embedded in the ISR rewrites is a random per-build value,
30+ // so normalize it to a stable placeholder before comparing.
31+ const routes = deploymentConfig . routes . slice ( 2 ) . map ( ( route : Record < string , unknown > ) => {
32+ if ( typeof route . dest === 'string' ) {
33+ return {
34+ ...route ,
35+ dest : route . dest . replace ( / x _ a s t r o _ p a t h _ t o k e n = [ ^ & ] + / , 'x_astro_path_token=$TOKEN' ) ,
36+ } ;
37+ }
38+ return route ;
39+ } ) ;
2940 // the first two are /_astro/*, and filesystem routes
30- assert . deepEqual ( deploymentConfig . routes . slice ( 2 ) , [
41+ assert . deepEqual ( routes , [
3142 {
3243 src : '^/two$' ,
3344 dest : '_render' ,
@@ -58,11 +69,11 @@ describe('ISR', () => {
5869 } ,
5970 {
6071 src : '^/one/?$' ,
61- dest : '/_isr?x_astro_path=$0' ,
72+ dest : '/_isr?x_astro_path=$0&x_astro_path_token=$TOKEN ' ,
6273 } ,
6374 {
6475 src : '^/404/?$' ,
65- dest : '/_isr?x_astro_path=$0' ,
76+ dest : '/_isr?x_astro_path=$0&x_astro_path_token=$TOKEN ' ,
6677 } ,
6778 {
6879 dest : '_render' ,
@@ -71,4 +82,78 @@ describe('ISR', () => {
7182 } ,
7283 ] ) ;
7384 } ) ;
85+
86+ it ( 'allow-lists every query param used in the ISR rewrite' , { timeout : 30000 } , async ( ) => {
87+ // Vercel strips any query param not present in `allowQuery` before invoking
88+ // the ISR function. If the rewrite `dest` references a param that isn't
89+ // allow-listed (e.g. the path token), that param never reaches the
90+ // entrypoint and every ISR route 404s. Assert the two stay in sync.
91+ const prerenderConfig = JSON . parse (
92+ await fixture . readFile ( '../.vercel/output/functions/_isr.prerender-config.json' ) ,
93+ ) ;
94+ const token = await readPathToken ( ) ;
95+ const isrRoute = new URL (
96+ `https://example.com/_isr?x_astro_path=/one&x_astro_path_token=${ token } ` ,
97+ ) ;
98+ for ( const param of isrRoute . searchParams . keys ( ) ) {
99+ assert . ok (
100+ prerenderConfig . allowQuery . includes ( param ) ,
101+ `ISR rewrite param "${ param } " must be present in allowQuery` ,
102+ ) ;
103+ }
104+ } ) ;
105+
106+ async function loadIsrFunction ( ) {
107+ const functionConfig = JSON . parse (
108+ await fixture . readFile ( '../.vercel/output/functions/_isr.func/.vc-config.json' ) ,
109+ ) ;
110+ const functionEntry = new URL (
111+ `../.vercel/output/functions/_isr.func/${ functionConfig . handler } ` ,
112+ fixture . config . outDir ,
113+ ) ;
114+ return import ( functionEntry . href ) ;
115+ }
116+
117+ async function readPathToken ( ) {
118+ const deploymentConfig = JSON . parse ( await fixture . readFile ( '../.vercel/output/config.json' ) ) ;
119+ const isrRoute = deploymentConfig . routes . find (
120+ ( route : { dest ?: string } ) =>
121+ typeof route . dest === 'string' && route . dest . startsWith ( '/_isr?' ) ,
122+ ) ;
123+ return new URL ( isrRoute . dest , 'https://example.com' ) . searchParams . get ( 'x_astro_path_token' ) ;
124+ }
125+
126+ it ( 'ignores x_astro_path without a valid path token' , { timeout : 30000 } , async ( ) => {
127+ const isrFunction = await loadIsrFunction ( ) ;
128+ const response = await isrFunction . default . fetch (
129+ new Request ( 'https://example.com/_isr?x_astro_path=/one' ) ,
130+ ) ;
131+ // Without the build's token the override is ignored, so `/_isr` matches no
132+ // route and does not render the target page.
133+ assert . equal ( response . status , 404 ) ;
134+ assert . equal ( ( await response . text ( ) ) . includes ( '<h1>One</h1>' ) , false ) ;
135+ } ) ;
136+
137+ it ( 'ignores x_astro_path when only the x-vercel-isr header is set' , {
138+ timeout : 30000 ,
139+ } , async ( ) => {
140+ const isrFunction = await loadIsrFunction ( ) ;
141+ const response = await isrFunction . default . fetch (
142+ new Request ( 'https://example.com/_isr?x_astro_path=/one' , {
143+ headers : { 'x-vercel-isr' : '1' } ,
144+ } ) ,
145+ ) ;
146+ assert . equal ( response . status , 404 ) ;
147+ assert . equal ( ( await response . text ( ) ) . includes ( '<h1>One</h1>' ) , false ) ;
148+ } ) ;
149+
150+ it ( 'honors x_astro_path when the valid path token is present' , { timeout : 30000 } , async ( ) => {
151+ const isrFunction = await loadIsrFunction ( ) ;
152+ const token = await readPathToken ( ) ;
153+ const response = await isrFunction . default . fetch (
154+ new Request ( `https://example.com/_isr?x_astro_path=/one&x_astro_path_token=${ token } ` ) ,
155+ ) ;
156+ assert . equal ( response . status , 200 ) ;
157+ assert . equal ( ( await response . text ( ) ) . includes ( '<h1>One</h1>' ) , true ) ;
158+ } ) ;
74159} ) ;
0 commit comments