@@ -108,6 +108,20 @@ impl GptIntegration {
108108 }
109109 }
110110
111+ /// Build the upstream URL for a proxied GPT request.
112+ ///
113+ /// Strips the integration prefix from the request path and constructs
114+ /// a full URL on the GPT host, preserving the original path and query.
115+ ///
116+ /// Returns `None` if `request_path` does not start with [`ROUTE_PREFIX`].
117+ fn build_upstream_url ( request_path : & str , query : Option < & str > ) -> Option < String > {
118+ let upstream_path = request_path. strip_prefix ( ROUTE_PREFIX ) ?;
119+ let query_part = query. map ( |q| format ! ( "?{}" , q) ) . unwrap_or_default ( ) ;
120+ Some ( format ! (
121+ "https://{SECUREPUBADS_HOST}{upstream_path}{query_part}"
122+ ) )
123+ }
124+
111125 /// Check if a URL points at Google's GPT bootstrap script (`gpt.js`).
112126 ///
113127 /// Only matches the canonical host:
@@ -184,13 +198,9 @@ impl GptIntegration {
184198 )
185199 . with_header (
186200 header:: CACHE_CONTROL ,
187- format ! (
188- "public, max-age={}, immutable" ,
189- self . config. cache_ttl_seconds
190- ) ,
201+ format ! ( "public, max-age={}" , self . config. cache_ttl_seconds) ,
191202 )
192203 . with_header ( "X-GPT-Proxy" , "true" )
193- . with_header ( "X-Script-Source" , script_url)
194204 . with_body ( body) ;
195205
196206 Self :: copy_content_encoding_headers ( & gpt_response, & mut response) ;
@@ -210,20 +220,11 @@ impl GptIntegration {
210220 req : Request ,
211221 ) -> Result < Response , Report < TrustedServerError > > {
212222 let original_path = req. get_path ( ) ;
223+ let query = req. get_url ( ) . query ( ) ;
213224
214- // Strip the integration prefix to recover the upstream path.
215- let upstream_path = original_path
216- . strip_prefix ( ROUTE_PREFIX )
225+ let target_url = Self :: build_upstream_url ( original_path, query)
217226 . ok_or_else ( || Self :: error ( format ! ( "Invalid GPT pagead path: {}" , original_path) ) ) ?;
218227
219- let query = req
220- . get_url ( )
221- . query ( )
222- . map ( |q| format ! ( "?{}" , q) )
223- . unwrap_or_default ( ) ;
224-
225- let target_url = format ! ( "https://{SECUREPUBADS_HOST}{upstream_path}{query}" ) ;
226-
227228 log:: info!( "GPT proxy: forwarding to {}" , target_url) ;
228229
229230 let mut upstream_req = Request :: new ( Method :: GET , & target_url) ;
@@ -269,13 +270,9 @@ impl GptIntegration {
269270 . with_header ( header:: CONTENT_TYPE , & content_type)
270271 . with_header (
271272 header:: CACHE_CONTROL ,
272- format ! (
273- "public, max-age={}, immutable" ,
274- self . config. cache_ttl_seconds
275- ) ,
273+ format ! ( "public, max-age={}" , self . config. cache_ttl_seconds) ,
276274 )
277275 . with_header ( "X-GPT-Proxy" , "true" )
278- . with_header ( "X-Script-Source" , & target_url)
279276 . with_body ( body) ;
280277
281278 Self :: copy_content_encoding_headers ( & upstream_response, & mut response) ;
@@ -772,4 +769,109 @@ mod tests {
772769 "should not build when integration is disabled"
773770 ) ;
774771 }
772+
773+ // -- Upstream URL building --
774+
775+ #[ test]
776+ fn build_upstream_url_strips_prefix_and_preserves_path ( ) {
777+ let url = GptIntegration :: build_upstream_url (
778+ "/integrations/gpt/pagead/managed/js/gpt/current/pubads_impl.js" ,
779+ None ,
780+ ) ;
781+ assert_eq ! (
782+ url. as_deref( ) ,
783+ Some ( "https://securepubads.g.doubleclick.net/pagead/managed/js/gpt/current/pubads_impl.js" ) ,
784+ "should strip the integration prefix and build the upstream URL"
785+ ) ;
786+ }
787+
788+ #[ test]
789+ fn build_upstream_url_preserves_query_string ( ) {
790+ let url = GptIntegration :: build_upstream_url (
791+ "/integrations/gpt/pagead/managed/js/gpt/current/pubads_impl.js" ,
792+ Some ( "cb=123&foo=bar" ) ,
793+ ) ;
794+ assert_eq ! (
795+ url. as_deref( ) ,
796+ Some ( "https://securepubads.g.doubleclick.net/pagead/managed/js/gpt/current/pubads_impl.js?cb=123&foo=bar" ) ,
797+ "should preserve the query string in the upstream URL"
798+ ) ;
799+ }
800+
801+ #[ test]
802+ fn build_upstream_url_handles_tag_routes ( ) {
803+ let url =
804+ GptIntegration :: build_upstream_url ( "/integrations/gpt/tag/js/gpt.js" , Some ( "v=2" ) ) ;
805+ assert_eq ! (
806+ url. as_deref( ) ,
807+ Some ( "https://securepubads.g.doubleclick.net/tag/js/gpt.js?v=2" ) ,
808+ "should handle /tag/* routes correctly"
809+ ) ;
810+ }
811+
812+ #[ test]
813+ fn build_upstream_url_returns_none_for_invalid_prefix ( ) {
814+ let url = GptIntegration :: build_upstream_url ( "/some/other/path" , None ) ;
815+ assert ! (
816+ url. is_none( ) ,
817+ "should return None when path does not start with the integration prefix"
818+ ) ;
819+ }
820+
821+ #[ test]
822+ fn build_upstream_url_handles_empty_path_after_prefix ( ) {
823+ let url = GptIntegration :: build_upstream_url ( "/integrations/gpt" , None ) ;
824+ assert_eq ! (
825+ url. as_deref( ) ,
826+ Some ( "https://securepubads.g.doubleclick.net" ) ,
827+ "should handle path that is exactly the prefix"
828+ ) ;
829+ }
830+
831+ // -- Vary header edge cases --
832+
833+ #[ test]
834+ fn vary_with_accept_encoding_wildcard ( ) {
835+ let result = GptIntegration :: vary_with_accept_encoding ( Some ( "*" ) ) ;
836+ assert_eq ! (
837+ result, "*" ,
838+ "should preserve Vary: * wildcard without appending Accept-Encoding"
839+ ) ;
840+ }
841+
842+ #[ test]
843+ fn vary_with_accept_encoding_case_insensitive ( ) {
844+ let result = GptIntegration :: vary_with_accept_encoding ( Some ( "Origin, ACCEPT-ENCODING" ) ) ;
845+ assert_eq ! (
846+ result, "Origin, ACCEPT-ENCODING" ,
847+ "should detect Accept-Encoding case-insensitively"
848+ ) ;
849+ }
850+
851+ #[ test]
852+ fn vary_with_accept_encoding_adds_when_missing ( ) {
853+ let result = GptIntegration :: vary_with_accept_encoding ( Some ( "Origin" ) ) ;
854+ assert_eq ! (
855+ result, "Origin, Accept-Encoding" ,
856+ "should append Accept-Encoding when not present"
857+ ) ;
858+ }
859+
860+ #[ test]
861+ fn vary_with_accept_encoding_empty_upstream ( ) {
862+ let result = GptIntegration :: vary_with_accept_encoding ( None ) ;
863+ assert_eq ! (
864+ result, "Accept-Encoding" ,
865+ "should use Accept-Encoding as default when upstream has no Vary"
866+ ) ;
867+ }
868+
869+ #[ test]
870+ fn vary_with_accept_encoding_empty_string ( ) {
871+ let result = GptIntegration :: vary_with_accept_encoding ( Some ( "" ) ) ;
872+ assert_eq ! (
873+ result, "Accept-Encoding" ,
874+ "should treat empty string the same as absent Vary"
875+ ) ;
876+ }
775877}
0 commit comments