From edc80f36ad4ce9f4bef5d45c8fca7c186bf48cc2 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Tue, 25 Aug 2026 12:39:26 +0530 Subject: [PATCH] Allow template caching behind edge auth --- .../src/middleware.rs | 7 +- .../src/middleware.rs | 7 +- .../src/middleware.rs | 7 +- .../src/middleware.rs | 7 +- crates/trusted-server-core/src/auth.rs | 268 +++++++++++++++--- crates/trusted-server-core/src/publisher.rs | 138 ++++++++- 6 files changed, 391 insertions(+), 43 deletions(-) diff --git a/crates/trusted-server-adapter-axum/src/middleware.rs b/crates/trusted-server-adapter-axum/src/middleware.rs index 45cbedc2c..37a94862d 100644 --- a/crates/trusted-server-adapter-axum/src/middleware.rs +++ b/crates/trusted-server-adapter-axum/src/middleware.rs @@ -66,8 +66,11 @@ impl AuthMiddleware { #[async_trait(?Send)] impl Middleware for AuthMiddleware { - async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { - match enforce_basic_auth(&self.settings, ctx.request()) { + async fn handle(&self, mut ctx: RequestContext, next: Next<'_>) -> Result { + // Takes the request mutably because `enforce_basic_auth` marks requests + // whose credential it consumed itself; the shared template cache gate + // reads that marker later. + match enforce_basic_auth(&self.settings, ctx.request_mut()) { Ok(Some(response)) => return Ok(response), Ok(None) => {} Err(report) => { diff --git a/crates/trusted-server-adapter-cloudflare/src/middleware.rs b/crates/trusted-server-adapter-cloudflare/src/middleware.rs index 5b605bcff..d1d8da87c 100644 --- a/crates/trusted-server-adapter-cloudflare/src/middleware.rs +++ b/crates/trusted-server-adapter-cloudflare/src/middleware.rs @@ -74,8 +74,11 @@ impl AuthMiddleware { #[async_trait(?Send)] impl Middleware for AuthMiddleware { - async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { - match enforce_basic_auth(&self.settings, ctx.request()) { + async fn handle(&self, mut ctx: RequestContext, next: Next<'_>) -> Result { + // Takes the request mutably because `enforce_basic_auth` marks requests + // whose credential it consumed itself; the shared template cache gate + // reads that marker later. + match enforce_basic_auth(&self.settings, ctx.request_mut()) { Ok(Some(response)) => return Ok(response), Ok(None) => {} Err(report) => { diff --git a/crates/trusted-server-adapter-fastly/src/middleware.rs b/crates/trusted-server-adapter-fastly/src/middleware.rs index 18f309c68..4712b1067 100644 --- a/crates/trusted-server-adapter-fastly/src/middleware.rs +++ b/crates/trusted-server-adapter-fastly/src/middleware.rs @@ -124,8 +124,11 @@ impl AuthMiddleware { #[async_trait(?Send)] impl Middleware for AuthMiddleware { - async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { - match enforce_basic_auth(&self.settings, ctx.request()) { + async fn handle(&self, mut ctx: RequestContext, next: Next<'_>) -> Result { + // Takes the request mutably because `enforce_basic_auth` marks requests + // whose credential it consumed itself; the shared template cache gate + // reads that marker later. + match enforce_basic_auth(&self.settings, ctx.request_mut()) { Ok(Some(response)) => return Ok(response), Ok(None) => {} Err(report) => { diff --git a/crates/trusted-server-adapter-spin/src/middleware.rs b/crates/trusted-server-adapter-spin/src/middleware.rs index 1bcede1fc..6eeca26a6 100644 --- a/crates/trusted-server-adapter-spin/src/middleware.rs +++ b/crates/trusted-server-adapter-spin/src/middleware.rs @@ -67,8 +67,11 @@ impl AuthMiddleware { #[async_trait(?Send)] impl Middleware for AuthMiddleware { - async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { - match enforce_basic_auth(&self.settings, ctx.request()) { + async fn handle(&self, mut ctx: RequestContext, next: Next<'_>) -> Result { + // Takes the request mutably because `enforce_basic_auth` marks requests + // whose credential it consumed itself; the shared template cache gate + // reads that marker later. + match enforce_basic_auth(&self.settings, ctx.request_mut()) { Ok(Some(response)) => return Ok(response), Ok(None) => {} Err(report) => { diff --git a/crates/trusted-server-core/src/auth.rs b/crates/trusted-server-core/src/auth.rs index 6c92d042d..68e590cd7 100644 --- a/crates/trusted-server-core/src/auth.rs +++ b/crates/trusted-server-core/src/auth.rs @@ -11,6 +11,46 @@ use crate::settings::Settings; const BASIC_AUTH_REALM: &str = r#"Basic realm="Trusted Server""#; +/// Marker recording that this request's `Authorization` header was consumed and +/// validated by a Trusted Server handler at the edge. +/// +/// The shared template cache refuses every request carrying `Authorization`, +/// because an authorized response must never become a reader-neutral template. +/// That rule exists for credentials bound for the publisher origin, whose +/// response content TS cannot reason about. +/// +/// A credential this edge terminated is a different case. [`enforce_basic_auth`] +/// runs as middleware ahead of routing, so a request that reaches a handler for a +/// gated path has necessarily already satisfied that same handler. Every reader +/// able to look up a template stored from such a request has authenticated +/// against the same credential, so reuse is not a cross-reader disclosure. +/// +/// Absence of this marker on a request that still carries `Authorization` means +/// the credential is pass-through, and the template cache continues to refuse it. +/// +/// # Invariants +/// +/// The private field makes [`enforce_basic_auth`] the only code that can produce +/// this marker. It grants shared-template eligibility to a request that would +/// otherwise be refused, so being unforgeable outside this module is the whole +/// point: a caller cannot assert "already authenticated" without having actually +/// checked. [`enforce_basic_auth`] also clears any inherited marker before it +/// decides, so the value can never outlive the check that produced it. +#[derive(Debug, Clone)] +pub(crate) struct EdgeTerminatedAuthorization(()); + +impl EdgeTerminatedAuthorization { + /// Builds the marker without performing a credential check. + /// + /// Test-only. Production code obtains this marker exclusively by passing + /// [`enforce_basic_auth`], which is what makes it meaningful. + #[cfg(test)] + #[must_use] + pub(crate) const fn for_test() -> Self { + Self(()) + } +} + /// Enforces HTTP Basic authentication for configured handler paths. /// /// Returns `Ok(None)` when the request does not target a protected handler or @@ -24,22 +64,49 @@ const BASIC_AUTH_REALM: &str = r#"Basic realm="Trusted Server""#; /// the reserved admin namespace fail closed if no handler matches, providing /// defense in depth for malformed and parameterized paths. /// +/// # Request mutation +/// +/// Takes `req` mutably because it owns [`EdgeTerminatedAuthorization`]. Any +/// inherited marker is cleared on entry, and a fresh one is inserted only on the +/// success path, so the marker present after this call always describes this +/// call's own decision. Nothing else about the request is touched — in +/// particular the `Authorization` header is left in place and still reaches the +/// publisher origin. +/// +/// That last point is a stated assumption: a credential this edge terminates is +/// treated as reader-neutral, which holds unless the origin *also* authenticates +/// on the same header. An origin that does so declares `Vary: Authorization`, +/// which the template-cache store refuses as an uncovered `Vary` name. An origin +/// that varies on `Authorization` without declaring it would defeat any HTTP +/// cache, and is out of scope here. +/// /// # Errors /// /// Returns an error when handler configuration is invalid, such as an /// un-compilable path regex. pub fn enforce_basic_auth( settings: &Settings, - req: &Request, + req: &mut Request, ) -> Result>, Report> { - let path = req.uri().path(); - let Some(handler) = settings.handler_for_path(path)? else { - if Settings::is_admin_path(path) { - return Err(Report::new(TrustedServerError::Configuration { - message: format!("Admin path `{path}` has no configured handler"), - })); - } - return Ok(None); + // Cleared before any early return so no inherited marker can survive a call + // that did not itself validate a credential. Without this, a request marked + // upstream and then routed to an unprotected path would keep an assertion + // nothing checked. + req.extensions_mut().remove::(); + + // Scoped so the path borrow ends before the successful branch marks the + // request. `handler` borrows `settings`, not `req`. + let handler = { + let path = req.uri().path(); + let Some(handler) = settings.handler_for_path(path)? else { + if Settings::is_admin_path(path) { + return Err(Report::new(TrustedServerError::Configuration { + message: format!("Admin path `{path}` has no configured handler"), + })); + } + return Ok(None); + }; + handler }; let Some((username, password)) = extract_credentials(req) else { @@ -59,6 +126,9 @@ pub fn enforce_basic_auth( .ct_eq(&Sha256::digest(password.as_bytes())); if bool::from(username_match & password_match) { + // Record that TS itself consumed this credential, so the shared template + // cache can distinguish it from a credential meant for the origin. + req.extensions_mut().insert(EdgeTerminatedAuthorization(())); Ok(None) } else { log::warn!("Basic auth failed for path: {}", req.uri().path()); @@ -67,10 +137,11 @@ pub fn enforce_basic_auth( } fn extract_credentials(req: &Request) -> Option<(String, String)> { - let header_value = req - .headers() - .get(header::AUTHORIZATION) - .and_then(|value| value.to_str().ok())?; + let mut header_values = req.headers().get_all(header::AUTHORIZATION).iter(); + let header_value = header_values.next()?.to_str().ok()?; + if header_values.next().is_some() { + return None; + } let mut parts = header_value.splitn(2, ' '); let scheme = parts.next()?.trim(); @@ -134,9 +205,9 @@ mod tests { let settings = create_test_settings(); for path in ["/_ts/admin%2Fec", "/_ts/admin%2fec"] { - let req = build_request(Method::GET, &format!("https://example.com{path}")); + let mut req = build_request(Method::GET, &format!("https://example.com{path}")); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .unwrap_or_else(|| panic!("should challenge {path}")); @@ -148,13 +219,142 @@ mod tests { } } + #[test] + fn valid_credentials_mark_the_request_as_edge_terminated() { + let settings = create_test_settings(); + let mut req = build_request(Method::GET, "https://example.com/secure"); + let encoded = STANDARD.encode("user:pass"); + set_authorization(&mut req, &format!("Basic {encoded}")); + + assert!( + enforce_basic_auth(&settings, &mut req) + .expect("should evaluate auth") + .is_none(), + "valid credentials should be admitted" + ); + assert!( + req.extensions() + .get::() + .is_some(), + "a credential this edge consumed should be marked so the template cache can share it" + ); + } + + #[test] + fn repeated_authorization_values_are_rejected_without_a_marker() { + let settings = create_test_settings(); + let mut req = build_request(Method::GET, "https://example.com/secure"); + let encoded = STANDARD.encode("user:pass"); + set_authorization(&mut req, &format!("Basic {encoded}")); + req.headers_mut().append( + header::AUTHORIZATION, + HeaderValue::from_static("Bearer publisher-origin-credential"), + ); + + let response = enforce_basic_auth(&settings, &mut req) + .expect("should evaluate auth") + .expect("should challenge an ambiguous credential"); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert!( + req.extensions() + .get::() + .is_none(), + "a repeated authorization field must remain pass-through rather than being marked safe" + ); + } + + #[test] + fn an_inherited_marker_is_cleared_on_an_unprotected_path() { + // The marker asserts "this edge already checked a credential". A request + // routed to a path no handler protects was never checked here, so a marker + // it arrived with must not survive to grant shared-template eligibility. + let settings = create_test_settings(); + let mut req = build_request(Method::GET, "https://example.com/open"); + set_authorization(&mut req, "Basic dXNlcjpwYXNz"); + req.extensions_mut() + .insert(EdgeTerminatedAuthorization::for_test()); + + assert!( + enforce_basic_auth(&settings, &mut req) + .expect("should evaluate auth") + .is_none(), + "an unprotected path should not challenge" + ); + assert!( + req.extensions() + .get::() + .is_none(), + "a marker no credential check produced must not survive this call" + ); + } + + #[test] + fn an_inherited_marker_is_cleared_when_credentials_are_rejected() { + let settings = create_test_settings(); + let mut req = build_request(Method::GET, "https://example.com/secure"); + let encoded = STANDARD.encode("user:wrong-pass"); + set_authorization(&mut req, &format!("Basic {encoded}")); + req.extensions_mut() + .insert(EdgeTerminatedAuthorization::for_test()); + + let response = enforce_basic_auth(&settings, &mut req) + .expect("should evaluate auth") + .expect("should challenge"); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert!( + req.extensions() + .get::() + .is_none(), + "a failed check must strip an inherited marker rather than honour it" + ); + } + + #[test] + fn a_non_protected_path_leaves_authorization_unmarked() { + let settings = create_test_settings(); + let mut req = build_request(Method::GET, "https://example.com/open"); + set_authorization(&mut req, "Basic dXNlcjpwYXNz"); + + assert!( + enforce_basic_auth(&settings, &mut req) + .expect("should evaluate auth") + .is_none(), + "an unprotected path should not challenge" + ); + assert!( + req.extensions() + .get::() + .is_none(), + "a credential no handler consumed is pass-through and must stay disqualifying" + ); + } + + #[test] + fn rejected_credentials_leave_the_request_unmarked() { + let settings = create_test_settings(); + let mut req = build_request(Method::GET, "https://example.com/secure"); + let encoded = STANDARD.encode("user:wrong-pass"); + set_authorization(&mut req, &format!("Basic {encoded}")); + + let response = enforce_basic_auth(&settings, &mut req) + .expect("should evaluate auth") + .expect("should challenge"); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert!( + req.extensions() + .get::() + .is_none(), + "a failed credential must never be marked as terminated" + ); + } + #[test] fn no_challenge_for_non_protected_path() { let settings = create_test_settings(); - let req = build_request(Method::GET, "https://example.com/open"); + let mut req = build_request(Method::GET, "https://example.com/open"); assert!( - enforce_basic_auth(&settings, &req) + enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .is_none() ); @@ -163,9 +363,9 @@ mod tests { #[test] fn challenge_when_missing_credentials() { let settings = create_test_settings(); - let req = build_request(Method::GET, "https://example.com/secure"); + let mut req = build_request(Method::GET, "https://example.com/secure"); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .expect("should challenge"); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -184,7 +384,7 @@ mod tests { set_authorization(&mut req, &format!("Basic {token}")); assert!( - enforce_basic_auth(&settings, &req) + enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .is_none() ); @@ -197,7 +397,7 @@ mod tests { let token = STANDARD.encode("wrong:wrong"); set_authorization(&mut req, &format!("Basic {token}")); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .expect("should challenge"); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -211,7 +411,7 @@ mod tests { let token = STANDARD.encode("wrong-user:pass"); set_authorization(&mut req, &format!("Basic {token}")); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .expect("should challenge"); assert_eq!( @@ -228,7 +428,7 @@ mod tests { let token = STANDARD.encode("user:wrong-pass"); set_authorization(&mut req, &format!("Basic {token}")); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .expect("should challenge"); assert_eq!( @@ -244,7 +444,7 @@ mod tests { let mut req = build_request(Method::GET, "https://example.com/secure"); set_authorization(&mut req, "Bearer token"); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .expect("should challenge"); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -269,7 +469,7 @@ mod tests { set_authorization(&mut req, &format!("Basic {token}")); assert!( - enforce_basic_auth(&settings, &req) + enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .is_none(), "should allow admin path with correct credentials" @@ -283,7 +483,7 @@ mod tests { let token = STANDARD.encode("admin:wrong"); set_authorization(&mut req, &format!("Basic {token}")); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .expect("should challenge admin path with wrong credentials"); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -312,9 +512,9 @@ mod tests { "https://example.com/_ts/page-bids?path=/article", "https://example.com/_ts/api/v1/identify", ] { - let req = build_request(Method::GET, path); + let mut req = build_request(Method::GET, path); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .unwrap_or_else(|| panic!("should challenge {path} under a `^/_ts` handler")); assert_eq!( @@ -328,9 +528,9 @@ mod tests { #[test] fn challenge_admin_path_with_missing_credentials() { let settings = create_test_settings(); - let req = build_request(Method::POST, "https://example.com/_ts/admin/keys/rotate"); + let mut req = build_request(Method::POST, "https://example.com/_ts/admin/keys/rotate"); - let response = enforce_basic_auth(&settings, &req) + let response = enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .expect("should challenge admin path with missing credentials"); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -354,12 +554,12 @@ mod tests { let settings: Settings = toml::from_str(&config).expect("should deserialize settings without finalization"); let ec_id = format!("{}.abc123", "a".repeat(64)); - let req = build_request( + let mut req = build_request( Method::GET, &format!("https://example.com/_ts/admin/ec/{ec_id}"), ); - let error = enforce_basic_auth(&settings, &req) + let error = enforce_basic_auth(&settings, &mut req) .expect_err("should fail closed without a matching admin handler"); assert!( error.to_string().contains("no configured handler"), @@ -375,10 +575,10 @@ mod tests { ); let settings: Settings = toml::from_str(&config).expect("should deserialize settings without finalization"); - let req = build_request(Method::GET, "https://example.com/_ts/administrator"); + let mut req = build_request(Method::GET, "https://example.com/_ts/administrator"); assert!( - enforce_basic_auth(&settings, &req) + enforce_basic_auth(&settings, &mut req) .expect("should evaluate auth") .is_none(), "should not classify a similar prefix as the admin namespace" diff --git a/crates/trusted-server-core/src/publisher.rs b/crates/trusted-server-core/src/publisher.rs index b0d63b82a..3cf6a76af 100644 --- a/crates/trusted-server-core/src/publisher.rs +++ b/crates/trusted-server-core/src/publisher.rs @@ -4343,7 +4343,22 @@ pub async fn handle_publisher_request( // Recorded before the request is consumed by the origin send: the template cache gate // below needs it, and an authorized response must never become a shared // template. - let request_had_authorization = req.headers().contains_key(header::AUTHORIZATION); + // + // A credential this edge already terminated is exempt. Basic auth runs as + // middleware ahead of routing, so reaching here on a gated path means the same + // handler already validated the request; every reader that can look the template + // up has satisfied that same credential. Without the marker the credential is + // pass-through to the origin and still disqualifies. See + // [`crate::auth::EdgeTerminatedAuthorization`]. + let authorization_value_count = req.headers().get_all(header::AUTHORIZATION).iter().count(); + let request_had_authorization = match authorization_value_count { + 0 => false, + 1 => req + .extensions() + .get::() + .is_none(), + _ => true, + }; let request_had_cookie = req.headers().contains_key(header::COOKIE); // Whether carrying a cookie is itself disqualifying. Computed once and used for both // the lookup and the store, so the two cannot drift apart. @@ -8973,6 +8988,127 @@ mod tests { ); } + #[tokio::test] + async fn a_pass_through_authorization_never_reaches_the_shared_template() { + let stub = Arc::new(StubHttpClient::new()); + let cache = Arc::new(MemoryTemplateCache::default()); + let settings = Arc::new(settings_with_mode("esi")); + let services = services(Arc::clone(&stub), Arc::clone(&cache)); + queue_shareable_html(&stub); + + let mut request = navigation_request(); + request.headers_mut().insert( + header::AUTHORIZATION, + HeaderValue::from_static("Basic dXNlcjpwYXNz"), + ); + + let response = run(&settings, &services, request).await; + assert_eq!( + response + .headers() + .get(HEADER_X_TS_TEMPLATE_CACHE) + .and_then(|value| value.to_str().ok()), + Some("bypass-request"), + "a credential TS did not terminate is bound for the origin, so its response must never be shared" + ); + assert_eq!( + cache.lookups.lock().expect("should lock lookups").len(), + 0, + "an unterminated authorization must bypass before the lookup, not after it" + ); + } + + #[tokio::test] + async fn repeated_authorization_never_reaches_the_shared_template() { + let stub = Arc::new(StubHttpClient::new()); + let cache = Arc::new(MemoryTemplateCache::default()); + let settings = Arc::new(settings_with_mode("esi")); + let services = services(Arc::clone(&stub), Arc::clone(&cache)); + queue_shareable_html(&stub); + + let mut request = navigation_request(); + request.headers_mut().insert( + header::AUTHORIZATION, + HeaderValue::from_static("Basic dXNlcjpwYXNz"), + ); + request.headers_mut().append( + header::AUTHORIZATION, + HeaderValue::from_static("Bearer publisher-origin-credential"), + ); + request + .extensions_mut() + .insert(crate::auth::EdgeTerminatedAuthorization::for_test()); + + let response = run(&settings, &services, request).await; + assert_eq!( + response + .headers() + .get(HEADER_X_TS_TEMPLATE_CACHE) + .and_then(|value| value.to_str().ok()), + Some("bypass-request"), + "an ambiguous authorization must remain bound for the origin even if a marker is present" + ); + assert_eq!( + cache.lookups.lock().expect("should lock lookups").len(), + 0, + "repeated authorization must bypass before the shared template lookup" + ); + } + + #[tokio::test] + async fn an_edge_terminated_authorization_still_shares_its_template() { + let stub = Arc::new(StubHttpClient::new()); + let cache = Arc::new(MemoryTemplateCache::default()); + let settings = Arc::new(settings_with_mode("esi")); + let services = services(Arc::clone(&stub), Arc::clone(&cache)); + + // One origin response for two requests: the warm read is asserted by the + // fixture running dry, exactly as in the unauthenticated case. + queue_shareable_html(&stub); + + let authorized_navigation = || { + let mut request = navigation_request(); + request.headers_mut().insert( + header::AUTHORIZATION, + HeaderValue::from_static("Basic dXNlcjpwYXNz"), + ); + request + .extensions_mut() + .insert(crate::auth::EdgeTerminatedAuthorization::for_test()); + request + }; + + let cold = run(&settings, &services, authorized_navigation()).await; + assert_eq!( + cold.headers() + .get(HEADER_X_TS_TEMPLATE_CACHE) + .and_then(|value| value.to_str().ok()), + Some("miss-stored"), + "a credential this edge terminated must be allowed to fill the shared template" + ); + let first = body_of(cold).await; + + let warm = run(&settings, &services, authorized_navigation()).await; + assert_eq!( + warm.headers() + .get(HEADER_X_TS_TEMPLATE_CACHE) + .and_then(|value| value.to_str().ok()), + Some("hit"), + "the second gated request must read the template the first one stored" + ); + let second = body_of(warm).await; + + assert_eq!( + stub.recorded_request_uris().len(), + 1, + "the warm gated request must not fetch the origin" + ); + assert_eq!( + second, first, + "the gated warm response must be byte-identical to the stored template" + ); + } + #[tokio::test] async fn only_the_cold_miss_uses_the_platform_assembler() { let stub = Arc::new(StubHttpClient::new());