@@ -22,6 +22,7 @@ use crate::error::TrustedServerError;
2222use crate :: platform:: {
2323 PlatformBackendSpec , PlatformHttpRequest , PlatformResponse , RuntimeServices , StoreName ,
2424} ;
25+ use crate :: redacted:: Redacted ;
2526use crate :: s3_sigv4:: { self , S3Credentials } ;
2627use crate :: settings:: {
2728 AssetOriginAuth , OriginQueryPolicy , ProxyAssetRoute , S3SigV4AuthConfig , Settings ,
@@ -69,6 +70,66 @@ const ASSET_PROXY_FORWARD_HEADERS: [header::HeaderName; 11] = [
6970const ASSET_PROXY_STRIP_RESPONSE_HEADERS : [ & str ; 3 ] =
7071 [ "set-cookie" , "strict-transport-security" , "clear-site-data" ] ;
7172
73+ /// Cache-control value used when asset proxy responses must not be stored.
74+ pub const ASSET_NO_STORE_PRIVATE_CACHE_CONTROL : & str = "no-store, private" ;
75+
76+ /// Cache policy metadata emitted by the asset proxy handler.
77+ ///
78+ /// The Fastly router finalizes standard response headers after the asset handler
79+ /// returns. This typed policy lets the router reapply protected cache directives
80+ /// without depending on an exact header string set by the handler.
81+ #[ derive( Clone , Copy , Debug , Eq , PartialEq ) ]
82+ pub enum AssetProxyCachePolicy {
83+ /// Leave origin/cache-control headers under normal response finalization.
84+ OriginControlled ,
85+ /// Reapply `Cache-Control: no-store, private` after standard finalization.
86+ NoStorePrivate ,
87+ }
88+
89+ impl AssetProxyCachePolicy {
90+ /// Apply protected cache headers after route-level response finalization.
91+ pub fn apply_after_route_finalization ( self , response : & mut Response ) {
92+ if self == Self :: NoStorePrivate {
93+ apply_no_store_cache_control ( response) ;
94+ }
95+ }
96+ }
97+
98+ /// Asset proxy response plus metadata needed by the outer router.
99+ pub struct AssetProxyResponse {
100+ response : Response ,
101+ cache_policy : AssetProxyCachePolicy ,
102+ }
103+
104+ impl AssetProxyResponse {
105+ fn new ( response : Response , cache_policy : AssetProxyCachePolicy ) -> Self {
106+ Self {
107+ response,
108+ cache_policy,
109+ }
110+ }
111+
112+ fn origin_controlled ( response : Response ) -> Self {
113+ Self :: new ( response, AssetProxyCachePolicy :: OriginControlled )
114+ }
115+
116+ fn no_store_private ( response : Response ) -> Self {
117+ Self :: new ( response, AssetProxyCachePolicy :: NoStorePrivate )
118+ }
119+
120+ /// Return cache policy metadata for router finalization.
121+ #[ must_use]
122+ pub fn cache_policy ( & self ) -> AssetProxyCachePolicy {
123+ self . cache_policy
124+ }
125+
126+ /// Consume this wrapper and return the Fastly response.
127+ #[ must_use]
128+ pub fn into_response ( self ) -> Response {
129+ self . response
130+ }
131+ }
132+
72133#[ derive( Clone , Debug , Eq , Hash , PartialEq ) ]
73134struct S3CredentialsCacheKey {
74135 secret_store : String ,
@@ -684,8 +745,8 @@ fn load_s3_credentials(
684745 . transpose ( ) ?;
685746 let credentials = Arc :: new ( S3Credentials {
686747 access_key_id,
687- secret_access_key,
688- session_token,
748+ secret_access_key : Redacted :: new ( secret_access_key ) ,
749+ session_token : session_token . map ( Redacted :: new ) ,
689750 } ) ;
690751
691752 let mut cache = S3_CREDENTIALS_CACHE
@@ -792,7 +853,7 @@ fn strip_asset_proxy_response_headers(response: &mut Response) {
792853fn apply_no_store_cache_control ( response : & mut Response ) {
793854 response. set_header (
794855 header:: CACHE_CONTROL ,
795- HeaderValue :: from_static ( "no-store, private" ) ,
856+ HeaderValue :: from_static ( ASSET_NO_STORE_PRIVATE_CACHE_CONTROL ) ,
796857 ) ;
797858}
798859
@@ -813,7 +874,7 @@ async fn preflight_s3_origin_for_image_optimizer(
813874 request_method : & Method ,
814875 unsigned_headers : & http:: HeaderMap ,
815876 backend_name : & str ,
816- ) -> Result < Option < Response > , Report < TrustedServerError > > {
877+ ) -> Result < Option < AssetProxyResponse > , Report < TrustedServerError > > {
817878 let Some ( auth @ AssetOriginAuth :: S3SigV4 ( _) ) = route. auth . as_ref ( ) else {
818879 return Ok ( None ) ;
819880 } ;
@@ -841,7 +902,7 @@ async fn preflight_s3_origin_for_image_optimizer(
841902 let mut response = head_response;
842903 strip_asset_proxy_response_headers ( & mut response) ;
843904 apply_no_store_cache_control ( & mut response) ;
844- return Ok ( Some ( response) ) ;
905+ return Ok ( Some ( AssetProxyResponse :: no_store_private ( response) ) ) ;
845906 }
846907
847908 let mut get_headers = unsigned_headers. clone ( ) ;
@@ -858,7 +919,7 @@ async fn preflight_s3_origin_for_image_optimizer(
858919 strip_asset_proxy_response_headers ( & mut response) ;
859920 apply_no_store_cache_control ( & mut response) ;
860921
861- Ok ( Some ( response) )
922+ Ok ( Some ( AssetProxyResponse :: no_store_private ( response) ) )
862923}
863924
864925/// Proxy a configured first-party asset path to its matched asset origin.
@@ -882,7 +943,7 @@ pub async fn handle_asset_proxy_request(
882943 services : & RuntimeServices ,
883944 req : Request ,
884945 route : & ProxyAssetRoute ,
885- ) -> Result < Response , Report < TrustedServerError > > {
946+ ) -> Result < AssetProxyResponse , Report < TrustedServerError > > {
886947 let incoming_query = req. get_query_str ( ) . unwrap_or ( "" ) ;
887948 let mut target_url = build_asset_proxy_target_url ( route, req. get_path ( ) , incoming_query) ?;
888949 let image_optimizer =
@@ -967,7 +1028,7 @@ pub async fn handle_asset_proxy_request(
9671028 let mut response = platform_response_to_fastly_asset ( platform_resp) . await ?;
9681029 strip_asset_proxy_response_headers ( & mut response) ;
9691030
970- Ok ( response)
1031+ Ok ( AssetProxyResponse :: origin_controlled ( response) )
9711032}
9721033
9731034/// Upserts the `ts-ec` query parameter on a URL, replacing any existing value.
@@ -1731,8 +1792,8 @@ mod tests {
17311792 clear_s3_credentials_cache_for_tests, handle_asset_proxy_request, handle_first_party_click,
17321793 handle_first_party_proxy, handle_first_party_proxy_rebuild, handle_first_party_proxy_sign,
17331794 is_host_allowed, proxy_request, rebuild_response_with_body,
1734- reconstruct_and_validate_signed_target, redirect_is_permitted, ProxyRequestConfig ,
1735- SUPPORTED_ENCODINGS ,
1795+ reconstruct_and_validate_signed_target, redirect_is_permitted, AssetProxyCachePolicy ,
1796+ ProxyRequestConfig , SUPPORTED_ENCODINGS ,
17361797 } ;
17371798 use crate :: constants:: { HEADER_ACCEPT , HEADER_X_FORWARDED_FOR } ;
17381799 use crate :: creative;
@@ -2761,7 +2822,8 @@ mod tests {
27612822 let route = ProxyAssetRoute :: new ( "/.images/" , "https://assets.example.com:8443" ) ;
27622823 let response = handle_asset_proxy_request ( & settings, & services, req, & route)
27632824 . await
2764- . expect ( "should proxy asset request" ) ;
2825+ . expect ( "should proxy asset request" )
2826+ . into_response ( ) ;
27652827 assert_eq ! ( response. get_status( ) , StatusCode :: OK ) ;
27662828
27672829 let all_headers = stub. recorded_request_headers ( ) ;
@@ -2852,7 +2914,8 @@ mod tests {
28522914 let route = ProxyAssetRoute :: new ( "/.images/" , "https://assets.example.com" ) ;
28532915 let response = handle_asset_proxy_request ( & settings, & services, req, & route)
28542916 . await
2855- . expect ( "should proxy asset request" ) ;
2917+ . expect ( "should proxy asset request" )
2918+ . into_response ( ) ;
28562919
28572920 assert ! (
28582921 response. get_header( header:: SET_COOKIE ) . is_none( ) ,
@@ -3152,7 +3215,8 @@ mod tests {
31523215
31533216 let mut response = handle_asset_proxy_request ( & settings, & services, req, & route)
31543217 . await
3155- . expect ( "should proxy optimized S3 asset request" ) ;
3218+ . expect ( "should proxy optimized S3 asset request" )
3219+ . into_response ( ) ;
31563220
31573221 assert_eq ! ( response. get_status( ) , StatusCode :: OK ) ;
31583222 assert_eq ! ( response. take_body_str( ) , "optimized" ) ;
@@ -3213,9 +3277,15 @@ mod tests {
32133277 ) ;
32143278 let route = test_s3_image_optimizer_route ( ) ;
32153279
3216- let mut response = handle_asset_proxy_request ( & settings, & services, req, & route)
3280+ let asset_response = handle_asset_proxy_request ( & settings, & services, req, & route)
32173281 . await
32183282 . expect ( "should return raw S3 error response" ) ;
3283+ assert_eq ! (
3284+ asset_response. cache_policy( ) ,
3285+ AssetProxyCachePolicy :: NoStorePrivate ,
3286+ "should carry a typed no-store policy for router finalization"
3287+ ) ;
3288+ let mut response = asset_response. into_response ( ) ;
32193289
32203290 assert_eq ! ( response. get_status( ) , StatusCode :: NOT_FOUND ) ;
32213291 assert_eq ! (
@@ -3271,7 +3341,8 @@ mod tests {
32713341
32723342 let mut response = handle_asset_proxy_request ( & settings, & services, req, & route)
32733343 . await
3274- . expect ( "should proxy debug S3 asset request" ) ;
3344+ . expect ( "should proxy debug S3 asset request" )
3345+ . into_response ( ) ;
32753346
32763347 assert_eq ! ( response. take_body_str( ) , "raw" ) ;
32773348 assert_eq ! (
@@ -3336,7 +3407,8 @@ mod tests {
33363407
33373408 let mut response = handle_asset_proxy_request ( & settings, & services, req, & route)
33383409 . await
3339- . expect ( "should proxy a streaming asset response" ) ;
3410+ . expect ( "should proxy a streaming asset response" )
3411+ . into_response ( ) ;
33403412
33413413 assert_eq ! ( response. take_body_str( ) , "chunk" ) ;
33423414 }
0 commit comments