Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 68 additions & 52 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use trusted_server_core::http_util::sanitize_forwarded_headers;
use trusted_server_core::integrations::IntegrationRegistry;
use trusted_server_core::platform::RuntimeServices;
use trusted_server_core::proxy::{
handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild,
handle_first_party_proxy_sign,
handle_asset_proxy_request, handle_first_party_click, handle_first_party_proxy,
handle_first_party_proxy_rebuild, handle_first_party_proxy_sign,
};
use trusted_server_core::publisher::{
handle_publisher_request, handle_tsjs_dynamic, stream_publisher_body, PublisherResponse,
Expand Down Expand Up @@ -150,6 +150,11 @@ async fn route_request(
let path = req.get_path().to_string();
let method = req.get_method().clone();

let matched_asset_route = match &method {
&Method::GET | &Method::HEAD => settings.asset_route_for_path(&path),
_ => None,
};

// Match known routes and handle them
let result = match (method, path.as_str()) {
// Serve the tsjs library
Expand Down Expand Up @@ -202,62 +207,73 @@ async fn route_request(
}))
}),

// No known route matched, proxy to publisher origin as fallback
// No known route matched, proxy to an asset origin or publisher origin as fallback
_ => {
log::info!(
"No known route matched for path: {}, proxying to publisher origin",
path
);

match runtime_services_for_consent_route(settings, runtime_services) {
Ok(publisher_services) => {
match handle_publisher_request(
settings,
integration_registry,
&publisher_services,
req,
) {
Ok(PublisherResponse::Stream {
mut response,
body,
params,
}) => {
// Streaming path: finalize headers, then stream body to client.
finalize_response(settings, geo_info.as_ref(), &mut response);
let mut streaming_body = response.stream_to_client();
if let Err(e) = stream_publisher_body(
if let Some(asset_route) = matched_asset_route {
log::info!(
"No explicit route matched for path: {}, proxying via asset route prefix {} to {}",
path,
asset_route.prefix,
asset_route.origin_url
);
handle_asset_proxy_request(settings, runtime_services, req, &asset_route.origin_url)
.await
} else {
log::info!(
"No known route matched for path: {}, proxying to publisher origin",
path
);

match runtime_services_for_consent_route(settings, runtime_services) {
Ok(publisher_services) => {
match handle_publisher_request(
settings,
integration_registry,
&publisher_services,
req,
) {
Ok(PublisherResponse::Stream {
mut response,
body,
&mut streaming_body,
&params,
settings,
integration_registry,
) {
// Headers already committed. Log and abort — client
// sees a truncated response. Standard proxy behavior.
log::error!("Streaming processing failed: {e:?}");
drop(streaming_body);
} else if let Err(e) = streaming_body.finish() {
log::error!("Failed to finish streaming body: {e}");
params,
}) => {
// Streaming path: finalize headers, then stream body to client.
finalize_response(settings, geo_info.as_ref(), &mut response);
let mut streaming_body = response.stream_to_client();
if let Err(e) = stream_publisher_body(
body,
&mut streaming_body,
&params,
settings,
integration_registry,
) {
// Headers already committed. Log and abort — client
// sees a truncated response. Standard proxy behavior.
log::error!("Streaming processing failed: {e:?}");
drop(streaming_body);
} else if let Err(e) = streaming_body.finish() {
log::error!("Failed to finish streaming body: {e}");
}
// Response already sent via stream_to_client()
return None;
}
Ok(PublisherResponse::PassThrough { mut response, body }) => {
// Binary pass-through: reattach body and send via send_to_client().
// This preserves Content-Length and avoids chunked encoding overhead.
// Fastly streams the body from its internal buffer — no WASM
// memory buffering occurs.
response.set_body(body);
Ok(response)
}
Ok(PublisherResponse::Buffered(response)) => Ok(response),
Err(e) => {
log::error!("Failed to proxy to publisher origin: {:?}", e);
Err(e)
}
// Response already sent via stream_to_client()
return None;
}
Ok(PublisherResponse::PassThrough { mut response, body }) => {
// Binary pass-through: reattach body and send via send_to_client().
// This preserves Content-Length and avoids chunked encoding overhead.
// Fastly streams the body from its internal buffer — no WASM
// memory buffering occurs.
response.set_body(body);
Ok(response)
}
Ok(PublisherResponse::Buffered(response)) => Ok(response),
Err(e) => {
log::error!("Failed to proxy to publisher origin: {:?}", e);
Err(e)
}
}
Err(e) => Err(e),
}
Err(e) => Err(e),
}
}
};
Expand Down
91 changes: 90 additions & 1 deletion crates/trusted-server-adapter-fastly/src/route_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use trusted_server_core::platform::{
StoreName,
};
use trusted_server_core::request_signing::JWKS_CONFIG_STORE_NAME;
use trusted_server_core::settings::Settings;
use trusted_server_core::settings::{ProxyAssetRoute, Settings};

use super::route_request;

Expand Down Expand Up @@ -249,3 +249,92 @@ fn configured_missing_consent_store_only_breaks_consent_routes() {
"should scope consent store failures to the consent-dependent routes"
);
}

#[test]
fn asset_routes_bypass_publisher_consent_dependencies() {
let mut settings = create_test_settings();
settings.proxy.asset_routes = vec![ProxyAssetRoute {
prefix: "/.images/".to_string(),
origin_url: "https://assets.example.com".to_string(),
}];
let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator");
let integration_registry =
IntegrationRegistry::new(&settings).expect("should create integration registry");

let asset_req = Request::get("https://test.com/.images/logo.png?auto=webp");
let asset_services = test_runtime_services(&asset_req);
let asset_resp = futures::executor::block_on(route_request(
&settings,
&orchestrator,
&integration_registry,
&asset_services,
asset_req,
))
.expect("should return an error response for asset proxy requests");
assert_eq!(
asset_resp.get_status(),
StatusCode::BAD_GATEWAY,
"should bypass publisher consent dependencies and fail only on the missing upstream client"
);
}

#[test]
fn built_in_routes_take_precedence_over_asset_routes() {
let mut settings = create_test_settings();
settings.proxy.asset_routes = vec![ProxyAssetRoute {
prefix: "/.well-known/".to_string(),
origin_url: "https://assets.example.com".to_string(),
}];
let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator");
let integration_registry =
IntegrationRegistry::new(&settings).expect("should create integration registry");

let req = Request::get("https://test.com/.well-known/trusted-server.json");
let services = test_runtime_services(&req);
let resp = futures::executor::block_on(route_request(
&settings,
&orchestrator,
&integration_registry,
&services,
req,
))
.expect("should route discovery request");
assert_eq!(
resp.get_status(),
StatusCode::OK,
"should keep explicit built-in routes ahead of asset routes"
);
}

#[test]
fn integration_routes_take_precedence_over_asset_routes() {
let mut settings = create_test_settings();
settings.proxy.asset_routes = vec![ProxyAssetRoute {
prefix: "/prebid.js".to_string(),
origin_url: "https://assets.example.com".to_string(),
}];
let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator");
let integration_registry =
IntegrationRegistry::new(&settings).expect("should create integration registry");

let req = Request::get("https://test.com/prebid.js");
let services = test_runtime_services(&req);
let mut resp = futures::executor::block_on(route_request(
&settings,
&orchestrator,
&integration_registry,
&services,
req,
))
.expect("should route integration request");
assert_eq!(
resp.get_status(),
StatusCode::OK,
"should keep explicit integration routes ahead of asset routes"
);
assert_eq!(
resp.take_body_str(),
"// Script overridden by Trusted Server\n",
"should serve the integration response instead of proxying to the asset origin"
);
}
Loading
Loading