Skip to content
32 changes: 31 additions & 1 deletion crates/trusted-server-adapter-fastly/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,12 @@ fn fastly_response_to_platform(
// FastlyPlatformHttpClient
// ---------------------------------------------------------------------------

fn apply_fastly_cache_bypass(request: &mut fastly::Request, bypass_cache: bool) {
if bypass_cache {
request.set_pass(true);
}
}

/// Fastly implementation of [`PlatformHttpClient`].
///
/// - [`send`](PlatformHttpClient::send) converts the platform request to a
Expand All @@ -529,10 +535,12 @@ impl PlatformHttpClient for FastlyPlatformHttpClient {
let backend_name = request.backend_name.clone();
let image_optimizer = request.image_optimizer;
let stream_response = request.stream_response;
let bypass_cache = request.bypass_cache;
let mut fastly_req = edge_request_to_fastly(request.request)?;
if let Some(options) = image_optimizer {
apply_fastly_image_optimizer(&mut fastly_req, options)?;
}
apply_fastly_cache_bypass(&mut fastly_req, bypass_cache);
let fastly_resp = fastly_req
.send(&backend_name)
.change_context(PlatformError::HttpClient)?;
Expand All @@ -552,7 +560,9 @@ impl PlatformHttpClient for FastlyPlatformHttpClient {
return Err(Report::new(PlatformError::HttpClient)
.attach("streaming responses are not supported with Fastly send_async"));
}
let fastly_req = edge_request_to_fastly(request.request)?;
let bypass_cache = request.bypass_cache;
let mut fastly_req = edge_request_to_fastly(request.request)?;
apply_fastly_cache_bypass(&mut fastly_req, bypass_cache);
let pending = fastly_req
.send_async(&backend_name)
.change_context(PlatformError::HttpClient)?;
Expand Down Expand Up @@ -873,6 +883,26 @@ mod tests {

// --- FastlyPlatformHttpClient -------------------------------------------

#[test]
fn apply_fastly_cache_bypass_sets_pass_when_enabled() {
let mut request = fastly::Request::get("https://example.com/");
apply_fastly_cache_bypass(&mut request, true);
assert!(
format!("{request:?}").contains("cache_override: Pass"),
"enabled bypass should select Fastly pass mode"
);
}

#[test]
fn apply_fastly_cache_bypass_preserves_default_when_disabled() {
let mut request = fastly::Request::get("https://example.com/");
apply_fastly_cache_bypass(&mut request, false);
assert!(
format!("{request:?}").contains("cache_override: None"),
"disabled bypass should preserve Fastly read-through caching"
);
}

#[test]
fn fastly_platform_http_client_send_returns_error_for_unregistered_backend() {
let client = FastlyPlatformHttpClient;
Expand Down
48 changes: 48 additions & 0 deletions crates/trusted-server-core/src/platform/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ pub struct PlatformHttpRequest {
/// Adapters that cannot attach this metadata to their send path should
/// return an error rather than silently dropping transformations.
pub image_optimizer: Option<PlatformImageOptimizerOptions>,
/// Whether the platform's intermediary response cache must be bypassed.
///
/// Adapters without an intermediary outbound cache may treat this as already
/// satisfied. The option defaults to `false` so existing call sites preserve
/// their current cache behavior.
pub bypass_cache: bool,
/// Whether the response body should stay streaming in the platform response.
///
/// Adapters that cannot preserve streaming response bodies should return an
Expand All @@ -38,6 +44,7 @@ impl PlatformHttpRequest {
request,
backend_name: backend_name.into(),
image_optimizer: None,
bypass_cache: false,
stream_response: false,
}
}
Expand All @@ -53,6 +60,13 @@ impl PlatformHttpRequest {
self
}

/// Bypass the platform's intermediary response cache for this request.
#[must_use]
pub fn with_cache_bypass(mut self) -> Self {
self.bypass_cache = true;
self
}

/// Preserve the upstream response body as a stream when the adapter supports it.
///
/// Asset routes use this to avoid materializing large image/static responses
Expand Down Expand Up @@ -306,8 +320,42 @@ pub trait PlatformHttpClient: Send + Sync {

#[cfg(test)]
mod tests {
use edgezero_core::body::Body;
use edgezero_core::http::request_builder;

use super::*;

#[test]
fn platform_http_request_cache_bypass_defaults_to_false() {
let request = PlatformHttpRequest::new(
request_builder()
.body(Body::empty())
.expect("should build request"),
"stub-backend",
);

assert!(
!request.bypass_cache,
"should preserve existing cache behavior by default"
);
}

#[test]
fn platform_http_request_cache_bypass_builder_enables_bypass() {
let request = PlatformHttpRequest::new(
request_builder()
.body(Body::empty())
.expect("should build request"),
"stub-backend",
)
.with_cache_bypass();

assert!(
request.bypass_cache,
"should enable intermediary cache bypass"
);
}

// ---------------------------------------------------------------------------
// Error-correlation interim scope (before EdgeZero #213)
// ---------------------------------------------------------------------------
Expand Down
33 changes: 31 additions & 2 deletions crates/trusted-server-core/src/platform/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub(crate) struct StubHttpClient {
// platforms whose send_async executes eagerly (e.g. Cloudflare Workers).
concurrent_fanout: std::sync::atomic::AtomicBool,
image_optimizer_options: Mutex<Vec<Option<PlatformImageOptimizerOptions>>>,
cache_bypass_flags: Mutex<Vec<bool>>,
stream_response_flags: Mutex<Vec<bool>>,
request_methods: Mutex<Vec<String>>,
request_uris: Mutex<Vec<String>>,
Expand All @@ -247,6 +248,7 @@ impl StubHttpClient {
select_errors: Mutex::new(VecDeque::new()),
concurrent_fanout: std::sync::atomic::AtomicBool::new(true),
image_optimizer_options: Mutex::new(Vec::new()),
cache_bypass_flags: Mutex::new(Vec::new()),
stream_response_flags: Mutex::new(Vec::new()),
request_methods: Mutex::new(Vec::new()),
request_uris: Mutex::new(Vec::new()),
Expand Down Expand Up @@ -319,6 +321,14 @@ impl StubHttpClient {
.clone()
}

/// Return cache-bypass flags captured per `send` or `send_async` call, in order.
pub(crate) fn recorded_cache_bypass_flags(&self) -> Vec<bool> {
self.cache_bypass_flags
.lock()
.expect("should lock cache bypass flags")
.clone()
}

/// Return streaming-response flags captured per `send` call, in order.
pub fn recorded_stream_response_flags(&self) -> Vec<bool> {
self.stream_response_flags
Expand Down Expand Up @@ -376,6 +386,10 @@ impl PlatformHttpClient for StubHttpClient {
.lock()
.expect("should lock image optimizer options")
.push(request.image_optimizer.clone());
self.cache_bypass_flags
.lock()
.expect("should lock cache bypass flags")
.push(request.bypass_cache);
self.stream_response_flags
.lock()
.expect("should lock stream response flags")
Expand Down Expand Up @@ -456,6 +470,10 @@ impl PlatformHttpClient for StubHttpClient {
.lock()
.expect("should lock calls")
.push(backend_name.clone());
self.cache_bypass_flags
.lock()
.expect("should lock cache bypass flags")
.push(request.bypass_cache);

let headers: Vec<(String, String)> = request
.request
Expand Down Expand Up @@ -742,6 +760,11 @@ mod tests {
vec!["stub-backend"],
"should record the backend name"
);
assert_eq!(
stub.recorded_cache_bypass_flags(),
vec![false],
"should record the default cache-bypass flag"
);
}

#[test]
Expand Down Expand Up @@ -787,8 +810,9 @@ mod tests {

let pending_a = futures::executor::block_on(stub.send_async(make_req("backend-a")))
.expect("should start request a");
let pending_b = futures::executor::block_on(stub.send_async(make_req("backend-b")))
.expect("should start request b");
let pending_b =
futures::executor::block_on(stub.send_async(make_req("backend-b").with_cache_bypass()))
.expect("should start request b");

assert_eq!(
pending_a.backend_name(),
Expand Down Expand Up @@ -827,6 +851,11 @@ mod tests {
vec!["backend-a", "backend-b"],
"should record both send_async calls in order"
);
assert_eq!(
stub.recorded_cache_bypass_flags(),
vec![false, true],
"should record both send_async cache-bypass flags in order"
);
}

#[test]
Expand Down
Loading
Loading