From d25b7a7b5ded6d6af34e4e6cf275ef592ab2b0e4 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 19 Jun 2026 10:36:17 -0500 Subject: [PATCH 1/7] Honor publisher origin host header override --- .../src/platform.rs | 27 +++++++++++++++++++ .../src/integrations/datadome/protection.rs | 1 + .../src/integrations/mod.rs | 1 + .../src/platform/test_support.rs | 1 + .../trusted-server-core/src/platform/types.rs | 2 ++ crates/trusted-server-core/src/proxy.rs | 2 ++ crates/trusted-server-core/src/publisher.rs | 1 + 7 files changed, 35 insertions(+) diff --git a/crates/trusted-server-adapter-fastly/src/platform.rs b/crates/trusted-server-adapter-fastly/src/platform.rs index 7e96a545..1a6347de 100644 --- a/crates/trusted-server-adapter-fastly/src/platform.rs +++ b/crates/trusted-server-adapter-fastly/src/platform.rs @@ -157,6 +157,7 @@ pub struct FastlyPlatformBackend; fn backend_config_from_spec(spec: &PlatformBackendSpec) -> BackendConfig<'_> { BackendConfig::new(&spec.scheme, &spec.host) .port(spec.port) + .host_header_override(spec.host_header_override.as_deref()) .certificate_check(spec.certificate_check) .first_byte_timeout(spec.first_byte_timeout) } @@ -619,6 +620,7 @@ mod tests { scheme: "https".to_string(), host: "origin.example.com".to_string(), port: None, + host_header_override: None, certificate_check: true, first_byte_timeout: Duration::from_secs(15), }; @@ -633,6 +635,28 @@ mod tests { ); } + #[test] + fn predict_name_includes_host_header_override_suffix() { + let backend = FastlyPlatformBackend; + let spec = PlatformBackendSpec { + scheme: "https".to_string(), + host: "origin.example.com".to_string(), + port: None, + host_header_override: Some("www.example.com".to_string()), + certificate_check: true, + first_byte_timeout: Duration::from_secs(15), + }; + + let name = backend + .predict_name(&spec) + .expect("should compute backend name for host header override"); + + assert_eq!( + name, "backend_https_origin_example_com_443_oh_www_example_com_t15000", + "should match BackendConfig naming convention with host header override" + ); + } + #[test] fn predict_name_includes_nocert_suffix_when_cert_check_disabled() { let backend = FastlyPlatformBackend; @@ -640,6 +664,7 @@ mod tests { scheme: "https".to_string(), host: "origin.example.com".to_string(), port: None, + host_header_override: None, certificate_check: false, first_byte_timeout: Duration::from_secs(15), }; @@ -661,6 +686,7 @@ mod tests { scheme: "https".to_string(), host: String::new(), port: None, + host_header_override: None, certificate_check: true, first_byte_timeout: Duration::from_secs(15), }; @@ -677,6 +703,7 @@ mod tests { scheme: "https".to_string(), host: "origin.example.com".to_string(), port: None, + host_header_override: None, certificate_check: true, first_byte_timeout: Duration::from_millis(2000), }; diff --git a/crates/trusted-server-core/src/integrations/datadome/protection.rs b/crates/trusted-server-core/src/integrations/datadome/protection.rs index 8512bcc9..96eade32 100644 --- a/crates/trusted-server-core/src/integrations/datadome/protection.rs +++ b/crates/trusted-server-core/src/integrations/datadome/protection.rs @@ -156,6 +156,7 @@ impl DataDomeIntegration { scheme: parsed.scheme().to_string(), host: host.to_string(), port: parsed.port(), + host_header_override: None, certificate_check: true, first_byte_timeout: Duration::from_millis(u64::from(self.config.timeout_ms)), }; diff --git a/crates/trusted-server-core/src/integrations/mod.rs b/crates/trusted-server-core/src/integrations/mod.rs index 297cf9d0..192857af 100644 --- a/crates/trusted-server-core/src/integrations/mod.rs +++ b/crates/trusted-server-core/src/integrations/mod.rs @@ -71,6 +71,7 @@ pub(crate) fn ensure_integration_backend( })? .to_string(), port: parsed.port(), + host_header_override: None, certificate_check: true, first_byte_timeout: first_byte_timeout.unwrap_or_else(|| Duration::from_secs(15)), }) diff --git a/crates/trusted-server-core/src/platform/test_support.rs b/crates/trusted-server-core/src/platform/test_support.rs index 36182fea..493135ce 100644 --- a/crates/trusted-server-core/src/platform/test_support.rs +++ b/crates/trusted-server-core/src/platform/test_support.rs @@ -807,6 +807,7 @@ mod tests { scheme: "https".to_string(), host: "example.com".to_string(), port: None, + host_header_override: None, certificate_check: true, first_byte_timeout: DEFAULT_FIRST_BYTE_TIMEOUT, }; diff --git a/crates/trusted-server-core/src/platform/types.rs b/crates/trusted-server-core/src/platform/types.rs index 33250d1d..25068218 100644 --- a/crates/trusted-server-core/src/platform/types.rs +++ b/crates/trusted-server-core/src/platform/types.rs @@ -133,6 +133,8 @@ pub struct PlatformBackendSpec { pub host: String, /// Explicit port, or `None` to use the scheme default. pub port: Option, + /// Optional outbound Host header to send to the backend origin. + pub host_header_override: Option, /// Whether to verify the TLS certificate. pub certificate_check: bool, /// Maximum time to wait for the first response byte. diff --git a/crates/trusted-server-core/src/proxy.rs b/crates/trusted-server-core/src/proxy.rs index bcd70e18..f60f2298 100644 --- a/crates/trusted-server-core/src/proxy.rs +++ b/crates/trusted-server-core/src/proxy.rs @@ -1058,6 +1058,7 @@ pub async fn handle_asset_proxy_request( scheme: scheme.to_string(), host: host.to_string(), port: target_url.port(), + host_header_override: None, certificate_check: settings.proxy.certificate_check, first_byte_timeout: DEFAULT_FIRST_BYTE_TIMEOUT, }) @@ -1241,6 +1242,7 @@ async fn proxy_with_redirects( scheme: scheme.clone(), host: host.to_string(), port: parsed_url.port(), + host_header_override: None, certificate_check: settings.proxy.certificate_check, first_byte_timeout: DEFAULT_FIRST_BYTE_TIMEOUT, }) diff --git a/crates/trusted-server-core/src/publisher.rs b/crates/trusted-server-core/src/publisher.rs index db8a1778..93ce75d6 100644 --- a/crates/trusted-server-core/src/publisher.rs +++ b/crates/trusted-server-core/src/publisher.rs @@ -499,6 +499,7 @@ pub async fn handle_publisher_request( scheme: origin_scheme.clone(), host: origin_host_without_port.to_string(), port: parsed_origin.port(), + host_header_override: settings.publisher.origin_host_header_override.clone(), certificate_check: settings.proxy.certificate_check, first_byte_timeout: DEFAULT_PUBLISHER_FIRST_BYTE_TIMEOUT, }) From 65a475c94a2c07cfb0be590aee93fe9ae4c843a2 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 19 Jun 2026 10:44:35 -0500 Subject: [PATCH 2/7] Replace forwarded host in Fastly request conversion --- .../src/platform.rs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/trusted-server-adapter-fastly/src/platform.rs b/crates/trusted-server-adapter-fastly/src/platform.rs index 1a6347de..4b1a9cf4 100644 --- a/crates/trusted-server-adapter-fastly/src/platform.rs +++ b/crates/trusted-server-adapter-fastly/src/platform.rs @@ -309,7 +309,11 @@ fn edge_request_to_fastly( let (parts, body) = request.into_parts(); let mut fastly_req = fastly::Request::new(parts.method, parts.uri.to_string()); for (name, value) in parts.headers.iter() { - fastly_req.append_header(name.as_str(), value.as_bytes()); + if name == edgezero_core::http::header::HOST { + fastly_req.set_header(name.as_str(), value.as_bytes()); + } else { + fastly_req.append_header(name.as_str(), value.as_bytes()); + } } match body { edgezero_core::body::Body::Once(bytes) => { @@ -611,6 +615,24 @@ mod tests { Arc::new(NoopKvStore) } + #[test] + fn edge_request_to_fastly_replaces_url_derived_host_header() { + let request = request_builder() + .method("GET") + .uri("https://origin.example.com/") + .header(edgezero_core::http::header::HOST, "www.example.com") + .body(Body::empty()) + .expect("should build request"); + + let fastly_req = edge_request_to_fastly(request).expect("should convert request"); + + assert_eq!( + fastly_req.get_header_str(fastly::http::header::HOST), + Some("www.example.com"), + "should replace the URL-derived Host instead of appending a duplicate" + ); + } + // --- FastlyPlatformBackend::predict_name -------------------------------- #[test] From 154b0359a83a156e0133d419e8128b0a55f6aea2 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 19 Jun 2026 11:15:53 -0500 Subject: [PATCH 3/7] Bypass Image Optimizer for incoming SVG asset paths --- crates/trusted-server-core/src/proxy.rs | 60 ++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/trusted-server-core/src/proxy.rs b/crates/trusted-server-core/src/proxy.rs index f60f2298..98528524 100644 --- a/crates/trusted-server-core/src/proxy.rs +++ b/crates/trusted-server-core/src/proxy.rs @@ -749,8 +749,8 @@ fn build_asset_proxy_target_url( Ok(target_url) } -fn asset_path_skips_image_optimizer(target_url: &url::Url) -> bool { - let lower_path = target_url.path().to_ascii_lowercase(); +fn asset_path_skips_image_optimizer(path: &str) -> bool { + let lower_path = path.to_ascii_lowercase(); lower_path.ends_with(".svg") || lower_path.ends_with(".svgz") } @@ -1028,12 +1028,15 @@ pub async fn handle_asset_proxy_request( req: Request, route: &ProxyAssetRoute, ) -> Result> { + let incoming_path = req.uri().path(); let incoming_query = req.uri().query().unwrap_or(""); - let mut target_url = build_asset_proxy_target_url(route, req.uri().path(), incoming_query)?; - let skip_image_optimizer = asset_path_skips_image_optimizer(&target_url); + let mut target_url = build_asset_proxy_target_url(route, incoming_path, incoming_query)?; + let skip_image_optimizer = asset_path_skips_image_optimizer(incoming_path) + || asset_path_skips_image_optimizer(target_url.path()); let image_optimizer = if skip_image_optimizer { log::debug!( - "Skipping Image Optimizer for unsupported SVG asset path: {}", + "Skipping Image Optimizer for unsupported SVG asset path: incoming={}, target={}", + incoming_path, target_url.path() ); None @@ -3096,7 +3099,7 @@ mod tests { ] { let target_url = url::Url::parse(url).expect("should parse target URL"); assert!( - asset_path_skips_image_optimizer(&target_url), + asset_path_skips_image_optimizer(target_url.path()), "should skip Image Optimizer for {url}" ); } @@ -3111,7 +3114,7 @@ mod tests { ] { let target_url = url::Url::parse(url).expect("should parse target URL"); assert!( - !asset_path_skips_image_optimizer(&target_url), + !asset_path_skips_image_optimizer(target_url.path()), "should allow Image Optimizer for {url}" ); } @@ -3664,6 +3667,49 @@ mod tests { ); } + #[tokio::test] + async fn handle_asset_proxy_request_skips_image_optimizer_for_incoming_svg() { + let stub = Arc::new(StubHttpClient::new()); + stub.push_response(200, b"ok".to_vec()); + let services = build_services_with_http_client( + Arc::clone(&stub) as Arc + ); + let mut settings = create_test_settings(); + let mut profile_set = test_profile_set(); + profile_set.unknown_profile = UnknownProfilePolicy::Reject; + settings.image_optimizer = ImageOptimizerSettings { + profile_sets: HashMap::from([("default_images".to_string(), profile_set)]), + }; + let req = build_http_request( + Method::GET, + "https://www.example.com/.image/object-id/logo.svg?profile=unknown&ar=1-1", + ); + let mut route = ProxyAssetRoute::new("/.image/", "https://assets.example.com"); + route.path_pattern = Some(r"^/\.image/([^/]+)/[^/]+\.([^/.]+)$".to_string()); + route.target_path = Some("/image/upload/$1".to_string()); + route.image_optimizer = Some(AssetImageOptimizerConfig { + enabled: true, + region: "us_east".to_string(), + profile_set: "default_images".to_string(), + origin_query: None, + }); + + handle_asset_proxy_request(&settings, &services, req, &route) + .await + .expect("should proxy incoming SVG asset without Image Optimizer profile parsing"); + + assert_eq!( + stub.recorded_request_uris(), + vec!["https://assets.example.com/image/upload/object-id"], + "should strip profile-table query even when SVG target path omits extension" + ); + assert_eq!( + stub.recorded_image_optimizer_options(), + vec![None], + "incoming SVG assets should bypass Image Optimizer metadata" + ); + } + #[tokio::test] async fn handle_asset_proxy_request_skips_s3_preflight_for_svg_image_optimizer_routes() { let stub = Arc::new(StubHttpClient::new()); From f405735aa378c65255c92f5d98eb709895b94963 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 19 Jun 2026 12:19:18 -0500 Subject: [PATCH 4/7] Load Prebid split bundle synchronously --- .../trusted-server-core/src/html_processor.rs | 6 +-- .../src/integrations/prebid.rs | 2 +- .../src/integrations/registry.rs | 10 +++-- crates/trusted-server-core/src/publisher.rs | 8 ++-- crates/trusted-server-core/src/tsjs.rs | 39 +++++++++++++++---- docs/guide/integration-guide.md | 14 +++---- 6 files changed, 53 insertions(+), 26 deletions(-) diff --git a/crates/trusted-server-core/src/html_processor.rs b/crates/trusted-server-core/src/html_processor.rs index eafe1e1e..18d56366 100644 --- a/crates/trusted-server-core/src/html_processor.rs +++ b/crates/trusted-server-core/src/html_processor.rs @@ -232,11 +232,11 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso for insert in integrations.head_inserts(&ctx) { snippet.push_str(&insert); } - // Main bundle: core + non-deferred integrations (synchronous). + // Main bundle: core + non-split integrations (synchronous). let immediate_ids = integrations.js_module_ids_immediate(); snippet.push_str(&tsjs::tsjs_script_tag(&immediate_ids)); - // Deferred bundles: large modules like prebid loaded after - // HTML parsing completes. Empty when none are enabled. + // Split bundles: large modules like prebid load outside the + // main bundle. Empty when none are enabled. let deferred_ids = integrations.js_module_ids_deferred(); snippet.push_str(&tsjs::tsjs_deferred_script_tags(&deferred_ids)); el.prepend(&snippet, ContentType::Html); diff --git a/crates/trusted-server-core/src/integrations/prebid.rs b/crates/trusted-server-core/src/integrations/prebid.rs index 8a2c6157..17b97d1d 100644 --- a/crates/trusted-server-core/src/integrations/prebid.rs +++ b/crates/trusted-server-core/src/integrations/prebid.rs @@ -1993,7 +1993,7 @@ passphrase = "test-secret-key-32-bytes-minimum" ); assert!( processed.contains("tsjs-prebid.min.js"), - "Deferred prebid bundle should be injected" + "Split prebid bundle should be injected" ); } diff --git a/crates/trusted-server-core/src/integrations/registry.rs b/crates/trusted-server-core/src/integrations/registry.rs index 99df1c73..9c6e24fc 100644 --- a/crates/trusted-server-core/src/integrations/registry.rs +++ b/crates/trusted-server-core/src/integrations/registry.rs @@ -699,8 +699,10 @@ impl IntegrationRegistrationBuilder { self } - /// Mark this integration's JS module for deferred loading via - /// `", + "", tsjs_deferred_script_src(module_id) ) } -/// Generate all deferred `"), + "should generate a synchronous prebid script tag" + ); + } + + #[test] + fn tsjs_deferred_script_tag_marks_other_split_modules_defer() { + let src = tsjs_deferred_script_src("creative"); + + assert_eq!( + tsjs_deferred_script_tag("creative"), format!(""), - "should generate a deferred script tag" + "should defer non-prebid split module script tags" ); } diff --git a/docs/guide/integration-guide.md b/docs/guide/integration-guide.md index 79576b8b..a0ffc12c 100644 --- a/docs/guide/integration-guide.md +++ b/docs/guide/integration-guide.md @@ -212,7 +212,7 @@ impl IntegrationScriptRewriter for MyIntegration { `html_processor.rs` calls these hooks after applying the standard origin→first-party rewrite, so you can simply swap URLs, append query parameters, or mutate inline JSON. Use this to point `", + "", tsjs_deferred_script_src(module_id) ) } -/// Generate all split module `"), - "should generate a synchronous prebid script tag" - ); - } - - #[test] - fn tsjs_deferred_script_tag_marks_other_split_modules_defer() { - let src = tsjs_deferred_script_src("creative"); - - assert_eq!( - tsjs_deferred_script_tag("creative"), format!(""), - "should defer non-prebid split module script tags" + "should generate a deferred script tag" ); } diff --git a/docs/guide/integration-guide.md b/docs/guide/integration-guide.md index a0ffc12c..79576b8b 100644 --- a/docs/guide/integration-guide.md +++ b/docs/guide/integration-guide.md @@ -212,7 +212,7 @@ impl IntegrationScriptRewriter for MyIntegration { `html_processor.rs` calls these hooks after applying the standard origin→first-party rewrite, so you can simply swap URLs, append query parameters, or mutate inline JSON. Use this to point `