Skip to content

Commit e3c03af

Browse files
Address cache policy review feedback
1 parent 52a2b24 commit e3c03af

13 files changed

Lines changed: 510 additions & 110 deletions

File tree

crates/trusted-server-adapter-cloudflare/wrangler.ci.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ compatibility_date = "2024-09-23"
44
compatibility_flags = ["nodejs_compat"]
55
# No [build] section — bundle is pre-built in CI; wrangler dev must not rebuild.
66

7+
[cache]
8+
enabled = true
9+
710
[[kv_namespaces]]
811
binding = "TRUSTED_SERVER_KV"
912
id = "ci-local-kv"

crates/trusted-server-adapter-cloudflare/wrangler.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ main = "build/index.js"
66
compatibility_date = "2024-09-23"
77
compatibility_flags = ["nodejs_compat"]
88

9+
[cache]
10+
enabled = true
11+
912
[build]
1013
command = "bash build.sh"
1114

crates/trusted-server-adapter-fastly/src/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ use edgezero_core::router::RouterService;
9595
use error_stack::Report;
9696
use trusted_server_core::auction::AuctionTelemetrySink;
9797
use trusted_server_core::auction::endpoints::handle_auction;
98-
use trusted_server_core::auction::{build_orchestrator, AuctionOrchestrator};
98+
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
9999
use trusted_server_core::cache_policy::EdgeCacheHeader;
100100
use trusted_server_core::constants::{COOKIE_SHAREDID, COOKIE_TS_EIDS};
101101
use trusted_server_core::ec::EcContext;

crates/trusted-server-adapter-fastly/src/main.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,11 @@ fn send_edgezero_response(
334334
effects.apply_to_response(&mut response);
335335
}
336336

337-
// Final cache guard: EC finalization and request-filter effects may have
338-
// added a per-user Set-Cookie after `apply_finalize_headers` ran, so
339-
// re-apply the privacy downgrade before send.
337+
// Final cache guards: EC finalization and request-filter effects may have
338+
// added a per-user Set-Cookie or a private/no-store directive after
339+
// `apply_finalize_headers` and normalized asset policy reapplication ran.
340340
crate::middleware::enforce_set_cookie_cache_privacy(&mut response);
341+
crate::middleware::enforce_uncacheable_cache_privacy(&mut response);
341342

342343
let (parts, body) = response.into_parts();
343344

crates/trusted-server-adapter-fastly/src/middleware.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,9 @@ pub(crate) fn apply_finalize_headers(
235235
/// entry point (`main.rs`) can re-apply it after
236236
/// [`ec_finalize_response`](trusted_server_core::ec::finalize::ec_finalize_response)
237237
/// writes the EC identity `Set-Cookie`, using the single shared implementation.
238-
pub(crate) use trusted_server_core::response_privacy::enforce_set_cookie_cache_privacy;
238+
pub(crate) use trusted_server_core::response_privacy::{
239+
enforce_set_cookie_cache_privacy, enforce_uncacheable_cache_privacy,
240+
};
239241

240242
// ---------------------------------------------------------------------------
241243
// Tests
@@ -496,6 +498,29 @@ mod tests {
496498
);
497499
}
498500

501+
#[test]
502+
fn enforce_uncacheable_cache_privacy_handles_late_filter_headers() {
503+
let mut response = response_with_headers(&[
504+
("cache-control", "private, max-age=0"),
505+
("surrogate-control", "max-age=600"),
506+
]);
507+
508+
enforce_uncacheable_cache_privacy(&mut response);
509+
510+
assert_eq!(
511+
response
512+
.headers()
513+
.get("cache-control")
514+
.and_then(|value| value.to_str().ok()),
515+
Some("private, max-age=0"),
516+
"should preserve the late private directive"
517+
);
518+
assert!(
519+
response.headers().get("surrogate-control").is_none(),
520+
"should strip the normalized edge header after late filter effects"
521+
);
522+
}
523+
499524
// ---------------------------------------------------------------------------
500525
// FinalizeResponseMiddleware::handle tests
501526
// ---------------------------------------------------------------------------

crates/trusted-server-core/src/proxy.rs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use std::time::Duration;
1212
use web_time::{SystemTime, UNIX_EPOCH};
1313

1414
use crate::cache_policy::{
15-
apply_no_store_private_to_headers, CachePolicy, EdgeCacheHeader, NO_STORE_PRIVATE_CACHE_CONTROL,
15+
CachePolicy, EdgeCacheHeader, NO_STORE_PRIVATE_CACHE_CONTROL,
16+
apply_no_store_private_to_headers, cache_control_headers_are_private_or_no_store,
17+
remove_edge_cache_headers,
1618
};
1719
use crate::constants::{
1820
HEADER_ACCEPT, HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_LANGUAGE, HEADER_REFERER,
@@ -125,7 +127,11 @@ impl AssetProxyCachePolicy {
125127
Self::OriginControlled => {}
126128
Self::NoStorePrivate => apply_no_store_cache_control(response),
127129
Self::Normalized(policy) => {
128-
policy.apply_to_headers(response.headers_mut(), edge_header)
130+
if cache_control_headers_are_private_or_no_store(response.headers()) {
131+
remove_edge_cache_headers(response.headers_mut());
132+
} else {
133+
policy.apply_to_headers(response.headers_mut(), edge_header);
134+
}
129135
}
130136
}
131137
}
@@ -3751,7 +3757,7 @@ mod tests {
37513757
}
37523758

37533759
#[test]
3754-
fn handle_asset_proxy_request_applies_configured_normalized_cache_policy() {
3760+
fn handle_asset_proxy_request_replaces_third_party_cache_policy_for_rehosted_asset() {
37553761
futures::executor::block_on(async {
37563762
let stub = Arc::new(StubHttpClient::new());
37573763
stub.push_response_with_headers(
@@ -3801,7 +3807,7 @@ mod tests {
38013807
assert_eq!(
38023808
response_header(&response, header::CACHE_CONTROL),
38033809
Some("public, max-age=31536000, immutable"),
3804-
"core response should apply browser cache policy immediately"
3810+
"configured rehost policy should replace the third-party no-store directive"
38053811
);
38063812
assert!(
38073813
response.headers().get("surrogate-control").is_none(),
@@ -3823,6 +3829,43 @@ mod tests {
38233829
});
38243830
}
38253831

3832+
#[test]
3833+
fn normalized_asset_policy_preserves_final_private_or_no_store_directives() {
3834+
for cache_control in ["private, max-age=0", "no-store"] {
3835+
let mut response = edge_response_builder()
3836+
.header(header::CACHE_CONTROL, cache_control)
3837+
.header("surrogate-control", "max-age=31536000")
3838+
.header("cdn-cache-control", "max-age=31536000")
3839+
.header("cloudflare-cdn-cache-control", "max-age=31536000")
3840+
.body(EdgeBody::empty())
3841+
.expect("should build asset response");
3842+
3843+
AssetProxyCachePolicy::Normalized(CachePolicy::public_immutable(Duration::from_secs(
3844+
31_536_000,
3845+
)))
3846+
.apply_after_route_finalization(&mut response, EdgeCacheHeader::SurrogateControl);
3847+
3848+
assert_eq!(
3849+
response
3850+
.headers()
3851+
.get(header::CACHE_CONTROL)
3852+
.and_then(|value| value.to_str().ok()),
3853+
Some(cache_control),
3854+
"final privacy directive should veto normalized cache policy"
3855+
);
3856+
assert!(
3857+
[
3858+
"surrogate-control",
3859+
"cdn-cache-control",
3860+
"cloudflare-cdn-cache-control",
3861+
]
3862+
.iter()
3863+
.all(|name| !response.headers().contains_key(*name)),
3864+
"final privacy directive should remove every edge-cache header"
3865+
);
3866+
}
3867+
}
3868+
38263869
#[test]
38273870
fn handle_asset_proxy_request_leaves_non_matching_assets_origin_controlled() {
38283871
futures::executor::block_on(async {

crates/trusted-server-core/src/publisher.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use crate::auction::types::{
4141
AuctionContext, AuctionRequest, Bid, DeviceInfo, PublisherInfo, SiteInfo, UserInfo,
4242
};
4343
use crate::cache_policy::{
44-
cache_control_headers_are_private_or_no_store, CachePolicy, EdgeCacheHeader,
44+
CachePolicy, EdgeCacheHeader, cache_control_headers_are_private_or_no_store,
4545
};
4646
use crate::consent::{consent_allows_server_side_auction, gate_eids_by_consent};
4747
use crate::constants::{COOKIE_TS_EIDS, HEADER_X_COMPRESS_HINT};

crates/trusted-server-core/src/response_privacy.rs

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,21 @@ use crate::cache_policy::{
1717
};
1818
use crate::settings::Settings;
1919

20-
/// Runtime edge-cache headers stripped from private or cookie-bearing responses.
21-
pub use crate::cache_policy::EDGE_CACHE_HEADER_NAMES as SURROGATE_CACHE_HEADERS;
22-
2320
fn cache_control_is_private_or_no_store(response: &Response) -> bool {
2421
cache_control_headers_are_private_or_no_store(response.headers())
2522
}
2623

24+
/// Removes runtime edge-cache headers from a response finalized as uncacheable.
25+
///
26+
/// Call this after any late response-header mutations so a final `private` or
27+
/// `no-store` directive cannot coexist with an independently authoritative edge
28+
/// cache header.
29+
pub fn enforce_uncacheable_cache_privacy(response: &mut Response) {
30+
if cache_control_is_private_or_no_store(response) {
31+
remove_edge_cache_headers(response.headers_mut());
32+
}
33+
}
34+
2735
/// Forces cookie-bearing responses to stay private to shared caches.
2836
///
2937
/// Any response that sets a per-user cookie (notably the EC identity cookie)
@@ -71,9 +79,7 @@ pub fn apply_response_headers_with_cache_privacy(settings: &Settings, response:
7179
enforce_set_cookie_cache_privacy(response);
7280

7381
let response_is_uncacheable = cache_control_is_private_or_no_store(response);
74-
if response_is_uncacheable {
75-
remove_edge_cache_headers(response.headers_mut());
76-
}
82+
enforce_uncacheable_cache_privacy(response);
7783

7884
for (key, value) in &settings.response_headers {
7985
if response_is_uncacheable
@@ -99,9 +105,7 @@ pub fn apply_response_headers_with_cache_privacy(settings: &Settings, response:
99105
response.headers_mut().insert(header_name, header_value);
100106
}
101107

102-
if cache_control_is_private_or_no_store(response) {
103-
remove_edge_cache_headers(response.headers_mut());
104-
}
108+
enforce_uncacheable_cache_privacy(response);
105109

106110
// Operator headers can themselves introduce Set-Cookie (alongside public
107111
// edge-cache headers) onto a previously cookieless response, which the
@@ -116,6 +120,8 @@ mod tests {
116120

117121
use edgezero_core::http::response_builder;
118122

123+
use crate::cache_policy::EDGE_CACHE_HEADER_NAMES;
124+
119125
fn settings_with_response_headers(headers: &[(&str, &str)]) -> Settings {
120126
let mut s = Settings::from_toml(
121127
r#"
@@ -267,6 +273,34 @@ mod tests {
267273
);
268274
}
269275

276+
#[test]
277+
fn final_uncacheable_guard_strips_edge_headers_without_a_cookie() {
278+
let mut response = response_builder()
279+
.header(header::CACHE_CONTROL, "no-store")
280+
.header("surrogate-control", "max-age=600")
281+
.header("cdn-cache-control", "max-age=600")
282+
.header("cloudflare-cdn-cache-control", "max-age=600")
283+
.body(edgezero_core::body::Body::empty())
284+
.expect("should build response");
285+
286+
enforce_uncacheable_cache_privacy(&mut response);
287+
288+
assert_eq!(
289+
response
290+
.headers()
291+
.get(header::CACHE_CONTROL)
292+
.and_then(|value| value.to_str().ok()),
293+
Some("no-store"),
294+
"final guard should preserve the uncacheable directive"
295+
);
296+
assert!(
297+
EDGE_CACHE_HEADER_NAMES
298+
.iter()
299+
.all(|name| !response.headers().contains_key(*name)),
300+
"final guard should remove every edge-cache header"
301+
);
302+
}
303+
270304
#[test]
271305
fn applies_operator_headers_on_cookieless_response() {
272306
let settings = settings_with_response_headers(&[("x-operator", "value")]);

0 commit comments

Comments
 (0)