@@ -831,8 +831,6 @@ describe('FetchState X-Forwarded-* header resolution', () => {
831831 } ) ;
832832
833833 it ( 'handles headers set by user fetch handler before FetchState creation' , ( ) => {
834- // This is the core use case from the issue: user sets forwarded
835- // headers in their src/app.ts fetch() before creating FetchState.
836834 const app = createTestApp ( [ createPage ( simplePage , { route : '/' } ) ] , {
837835 allowedDomains : [ { hostname : 'example.com' } ] ,
838836 } ) ;
@@ -849,8 +847,6 @@ describe('FetchState X-Forwarded-* header resolution', () => {
849847 } ) ;
850848
851849 it ( 'renders through the full pipeline with forwarded headers applied' , async ( ) => {
852- // End-to-end: forwarded headers should be visible in Astro.url
853- // when rendering through the astro() combined handler.
854850 const app = createTestApp ( [ createPage ( simplePage , { route : '/' } ) ] , {
855851 allowedDomains : [ { hostname : 'example.com' } ] ,
856852 } ) ;
@@ -865,13 +861,123 @@ describe('FetchState X-Forwarded-* header resolution', () => {
865861 ) ;
866862 const state = new FetchState ( request ) ;
867863
868- // Verify URL was updated before the handler runs
869864 assert . equal ( state . url . protocol , 'https:' ) ;
870865 assert . equal ( state . url . hostname , 'example.com' ) ;
871866
872867 const response = await astro ( state ) ;
873868 assert . equal ( response . status , 200 ) ;
874869 } ) ;
870+
871+ it ( 'updates request.url to match the forwarded URL' , ( ) => {
872+ const app = createTestApp ( [ createPage ( simplePage , { route : '/' } ) ] , {
873+ allowedDomains : [ { hostname : 'example.com' } ] ,
874+ } ) ;
875+ const request = stampApp (
876+ new Request ( 'http://localhost:4321/page' , {
877+ headers : {
878+ 'x-forwarded-proto' : 'https' ,
879+ 'x-forwarded-host' : 'example.com' ,
880+ } ,
881+ } ) ,
882+ app ,
883+ ) ;
884+ const state = new FetchState ( request ) ;
885+
886+ assert . equal ( state . url . protocol , 'https:' ) ;
887+ assert . equal ( state . url . hostname , 'example.com' ) ;
888+
889+ const requestUrl = new URL ( state . request . url ) ;
890+ assert . equal ( requestUrl . protocol , 'https:' ) ;
891+ assert . equal ( requestUrl . hostname , 'example.com' ) ;
892+ assert . equal ( requestUrl . pathname , '/page' ) ;
893+ } ) ;
894+
895+ it ( 'does not reconstruct request when no forwarded headers are validated' , ( ) => {
896+ const app = createTestApp ( [ createPage ( simplePage , { route : '/' } ) ] , {
897+ allowedDomains : [ { hostname : 'trusted.com' } ] ,
898+ } ) ;
899+ const original = new Request ( 'http://localhost:4321/' , {
900+ headers : {
901+ 'x-forwarded-host' : 'evil.com' ,
902+ } ,
903+ } ) ;
904+ const request = stampApp ( original , app ) ;
905+ const state = new FetchState ( request ) ;
906+
907+ // Rejected forwarded host — request should stay unchanged
908+ assert . equal ( state . url . hostname , 'localhost' ) ;
909+ assert . equal ( new URL ( state . request . url ) . hostname , 'localhost' ) ;
910+ } ) ;
911+
912+ it ( 'carries appSymbol onto the reconstructed request so the app still resolves' , async ( ) => {
913+ const app = createTestApp ( [ createPage ( simplePage , { route : '/' } ) ] , {
914+ allowedDomains : [ { hostname : 'example.com' } ] ,
915+ } ) ;
916+ const original = new Request ( 'http://localhost:4321/' , {
917+ headers : {
918+ 'x-forwarded-proto' : 'https' ,
919+ 'x-forwarded-host' : 'example.com' ,
920+ } ,
921+ } ) ;
922+ const request = stampApp ( original , app ) ;
923+ const state = new FetchState ( request ) ;
924+
925+ assert . notEqual ( state . request , original ) ;
926+ assert . equal ( Reflect . get ( state . request , appSymbol ) , app ) ;
927+ const response = await astro ( state ) ;
928+ assert . equal ( response . status , 200 ) ;
929+ } ) ;
930+
931+ it ( 'preserves method, headers and body when reconstructing for forwarded headers' , async ( ) => {
932+ const app = createTestApp ( [ createPage ( simplePage , { route : '/api' } ) ] , {
933+ allowedDomains : [ { hostname : 'example.com' } ] ,
934+ } ) ;
935+ const request = stampApp (
936+ new Request ( 'http://localhost:4321/api' , {
937+ method : 'POST' ,
938+ headers : {
939+ 'content-type' : 'application/x-www-form-urlencoded' ,
940+ 'x-forwarded-proto' : 'https' ,
941+ 'x-forwarded-host' : 'example.com' ,
942+ } ,
943+ body : 'token=abc123' ,
944+ } ) ,
945+ app ,
946+ ) ;
947+ const state = new FetchState ( request ) ;
948+
949+ assert . equal ( new URL ( state . request . url ) . protocol , 'https:' ) ;
950+ assert . equal ( new URL ( state . request . url ) . hostname , 'example.com' ) ;
951+ assert . equal ( state . request . method , 'POST' ) ;
952+ assert . equal ( state . request . headers . get ( 'content-type' ) , 'application/x-www-form-urlencoded' ) ;
953+ assert . equal ( await state . request . text ( ) , 'token=abc123' ) ;
954+ } ) ;
955+
956+ it ( 'preserves a streaming request body (duplex) when reconstructing' , async ( ) => {
957+ const app = createTestApp ( [ createPage ( simplePage , { route : '/api' } ) ] , {
958+ allowedDomains : [ { hostname : 'example.com' } ] ,
959+ } ) ;
960+ const body = new ReadableStream ( {
961+ start ( controller ) {
962+ controller . enqueue ( new TextEncoder ( ) . encode ( 'chunked-payload' ) ) ;
963+ controller . close ( ) ;
964+ } ,
965+ } ) ;
966+ const init : any = {
967+ method : 'POST' ,
968+ headers : {
969+ 'x-forwarded-proto' : 'https' ,
970+ 'x-forwarded-host' : 'example.com' ,
971+ } ,
972+ body,
973+ duplex : 'half' ,
974+ } ;
975+ const request = stampApp ( new Request ( 'http://localhost:4321/api' , init ) , app ) ;
976+ const state = new FetchState ( request ) ;
977+
978+ assert . equal ( new URL ( state . request . url ) . hostname , 'example.com' ) ;
979+ assert . equal ( await state . request . text ( ) , 'chunked-payload' ) ;
980+ } ) ;
875981} ) ;
876982
877983// #endregion
0 commit comments