@@ -235,6 +235,202 @@ Deno.test("App - methods with middleware", async () => {
235235 expect ( await res . text ( ) ) . toEqual ( "A" ) ;
236236} ) ;
237237
238+ Deno . test ( "App - ctx.rewrite() rematches and preserves state" , async ( ) => {
239+ const LOCALES = new Set ( [ "de" , "ru" ] ) ;
240+
241+ const app = new App < { locale ?: string } > ( )
242+ . use ( ( ctx ) => {
243+ const [ , first , ...rest ] = ctx . url . pathname . split ( "/" ) ;
244+
245+ if ( ctx . state . locale === undefined && LOCALES . has ( first ) ) {
246+ ctx . state . locale = first ;
247+ }
248+
249+ if ( LOCALES . has ( first ) ) {
250+ const rewritten = `/${ rest . join ( "/" ) } ` ;
251+ return ctx . rewrite ( rewritten === "/" ? "/" : rewritten ) ;
252+ }
253+
254+ if ( ctx . state . locale === undefined ) {
255+ ctx . state . locale = "en" ;
256+ }
257+
258+ return ctx . next ( ) ;
259+ } )
260+ . get ( "/hello" , ( ctx ) => {
261+ const q = ctx . url . searchParams . get ( "q" ) ?? "" ;
262+ return new Response (
263+ `${ ctx . state . locale } :${ ctx . route } :${ ctx . url . pathname } :${ q } ` ,
264+ ) ;
265+ } ) ;
266+
267+ const server = new FakeServer ( app . handler ( ) ) ;
268+
269+ let res = await server . get ( "/de/hello?q=1" ) ;
270+ expect ( await res . text ( ) ) . toEqual ( "de:/hello:/hello:1" ) ;
271+
272+ res = await server . get ( "/hello?q=2" ) ;
273+ expect ( await res . text ( ) ) . toEqual ( "en:/hello:/hello:2" ) ;
274+ } ) ;
275+
276+ Deno . test ( "App - ctx.rewrite() from route middleware" , async ( ) => {
277+ const app = new App ( )
278+ . get ( "/legacy" , ( ctx ) => ctx . rewrite ( "/modern" ) )
279+ . get ( "/modern" , ( ) => new Response ( "ok" ) ) ;
280+
281+ const server = new FakeServer ( app . handler ( ) ) ;
282+ const res = await server . get ( "/legacy" ) ;
283+ expect ( await res . text ( ) ) . toEqual ( "ok" ) ;
284+ } ) ;
285+
286+ Deno . test ( "App - ctx.rewrite() allows post-processing in middleware" , async ( ) => {
287+ const app = new App ( )
288+ . use ( async ( ctx ) => {
289+ if ( ctx . url . pathname === "/legacy" ) {
290+ const res = await ctx . rewrite ( "/modern" ) ;
291+ res . headers . set ( "x-rewritten" , "1" ) ;
292+ return res ;
293+ }
294+
295+ return ctx . next ( ) ;
296+ } )
297+ . get ( "/modern" , ( ) => new Response ( "ok" ) ) ;
298+
299+ const server = new FakeServer ( app . handler ( ) ) ;
300+ const res = await server . get ( "/legacy" ) ;
301+ expect ( await res . text ( ) ) . toEqual ( "ok" ) ;
302+ expect ( res . headers . get ( "x-rewritten" ) ) . toEqual ( "1" ) ;
303+ } ) ;
304+
305+ Deno . test ( "App - ctx.rewrite() preserves method and body" , async ( ) => {
306+ const app = new App ( )
307+ . use ( ( ctx ) => {
308+ if ( ctx . url . pathname === "/old" ) {
309+ return ctx . rewrite ( "/new" ) ;
310+ }
311+ return ctx . next ( ) ;
312+ } )
313+ . post ( "/new" , async ( ctx ) => {
314+ return new Response ( `${ ctx . req . method } :${ await ctx . req . text ( ) } ` ) ;
315+ } ) ;
316+
317+ const server = new FakeServer ( app . handler ( ) ) ;
318+ const res = await server . post ( "/old" , "payload" ) ;
319+ expect ( await res . text ( ) ) . toEqual ( "POST:payload" ) ;
320+ } ) ;
321+
322+ Deno . test ( "App - ctx.rewrite() preserves query by default" , async ( ) => {
323+ const app = new App ( )
324+ . get ( "/from" , ( ctx ) => ctx . rewrite ( "/to" ) )
325+ . get ( "/to" , ( ctx ) => new Response ( ctx . url . searchParams . get ( "q" ) ?? "" ) ) ;
326+
327+ const server = new FakeServer ( app . handler ( ) ) ;
328+ const res = await server . get ( "/from?q=123" ) ;
329+ expect ( await res . text ( ) ) . toEqual ( "123" ) ;
330+ } ) ;
331+
332+ Deno . test (
333+ "App - ctx.rewrite() query in target overrides current query" ,
334+ async ( ) => {
335+ const app = new App ( )
336+ . get ( "/from" , ( ctx ) => ctx . rewrite ( "/to?q=override" ) )
337+ . get (
338+ "/to" ,
339+ ( ctx ) => new Response ( ctx . url . searchParams . get ( "q" ) ?? "" ) ,
340+ ) ;
341+
342+ const server = new FakeServer ( app . handler ( ) ) ;
343+ const res = await server . get ( "/from?q=123" ) ;
344+ expect ( await res . text ( ) ) . toEqual ( "override" ) ;
345+ } ,
346+ ) ;
347+
348+ Deno . test ( "App - ctx.rewrite() supports URL targets with basePath" , async ( ) => {
349+ const app = new App ( { basePath : "/base" } )
350+ . get ( "/old" , ( ctx ) => ctx . rewrite ( new URL ( "/base/new?q=1" , ctx . url ) ) )
351+ . get (
352+ "/new" ,
353+ ( ctx ) =>
354+ new Response (
355+ `${ ctx . url . pathname } :${ ctx . url . searchParams . get ( "q" ) ?? "" } ` ,
356+ ) ,
357+ ) ;
358+
359+ const server = new FakeServer ( app . handler ( ) ) ;
360+ const res = await server . get ( "/base/old" ) ;
361+ expect ( await res . text ( ) ) . toEqual ( "/base/new:1" ) ;
362+ } ) ;
363+
364+ Deno . test ( "App - ctx.rewrite() throws on rewrite loops" , async ( ) => {
365+ const app = new App ( )
366+ . use ( async ( ctx ) => {
367+ try {
368+ return await ctx . next ( ) ;
369+ } catch ( err ) {
370+ return new Response ( String ( err ) , { status : 500 } ) ;
371+ }
372+ } )
373+ . use ( ( ctx ) => {
374+ if ( ctx . url . pathname === "/a" ) {
375+ return ctx . rewrite ( "/b" ) ;
376+ }
377+ if ( ctx . url . pathname === "/b" ) {
378+ return ctx . rewrite ( "/a" ) ;
379+ }
380+ return ctx . next ( ) ;
381+ } )
382+ . get ( "/a" , ( ) => new Response ( "a" ) )
383+ . get ( "/b" , ( ) => new Response ( "b" ) ) ;
384+
385+ const server = new FakeServer ( app . handler ( ) ) ;
386+ const res = await server . get ( "/a" ) ;
387+
388+ expect ( res . status ) . toEqual ( 500 ) ;
389+ expect ( await res . text ( ) ) . toContain ( "Too many internal rewrites" ) ;
390+ } ) ;
391+
392+ Deno . test ( "App - ctx.rewrite() rejects cross-origin targets" , async ( ) => {
393+ const app = new App ( )
394+ . use ( async ( ctx ) => {
395+ try {
396+ return await ctx . next ( ) ;
397+ } catch ( err ) {
398+ return new Response ( String ( err ) , { status : 500 } ) ;
399+ }
400+ } )
401+ . get ( "/" , ( ctx ) => ctx . rewrite ( "https://deno.land/" ) ) ;
402+
403+ const server = new FakeServer ( app . handler ( ) ) ;
404+ const res = await server . get ( "/" ) ;
405+
406+ expect ( res . status ) . toEqual ( 500 ) ;
407+ expect ( await res . text ( ) ) . toContain ( "only supports same-origin URLs" ) ;
408+ } ) ;
409+
410+ Deno . test ( "App - ctx.rewrite() rejects rewrites after body consumption" , async ( ) => {
411+ const app = new App ( )
412+ . use ( async ( ctx ) => {
413+ try {
414+ return await ctx . next ( ) ;
415+ } catch ( err ) {
416+ return new Response ( String ( err ) , { status : 500 } ) ;
417+ }
418+ } )
419+ . post ( "/" , async ( ctx ) => {
420+ await ctx . req . text ( ) ;
421+ return ctx . rewrite ( "/next" ) ;
422+ } )
423+ . post ( "/next" , ( ) => new Response ( "ok" ) ) ;
424+
425+ const server = new FakeServer ( app . handler ( ) ) ;
426+ const res = await server . post ( "/" , "payload" ) ;
427+
428+ expect ( res . status ) . toEqual ( 500 ) ;
429+ expect ( await res . text ( ) ) . toContain (
430+ "request after its body has already been consumed" ,
431+ ) ;
432+ } ) ;
433+
238434Deno . test ( "App - .mountApp() compose apps" , async ( ) => {
239435 const innerApp = new App < { text : string } > ( )
240436 . use ( ( ctx ) => {
0 commit comments