From 14eb5072e43a76215680169b6ff71dabb48a360c Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Sat, 27 Jun 2026 22:18:52 +0530 Subject: [PATCH 1/4] Add ConfigStoreUnavailable error variant mapping to 503 --- crates/trusted-server-core/src/error.rs | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/trusted-server-core/src/error.rs b/crates/trusted-server-core/src/error.rs index 2804afeca..ed934bb65 100644 --- a/crates/trusted-server-core/src/error.rs +++ b/crates/trusted-server-core/src/error.rs @@ -26,6 +26,11 @@ pub enum TrustedServerError { #[display("Configuration error: {message}")] Configuration { message: String }, + /// Config store could not be read (unseeded, transient backend, or a listed + /// key missing) — Settings cannot be loaded. Retryable / fix by seeding. + #[display("Config store unavailable: {store_name} - {message}")] + ConfigStoreUnavailable { store_name: String, message: String }, + /// Auction orchestration error. #[display("Auction error: {message}")] Auction { message: String }, @@ -123,6 +128,7 @@ impl IntoHttpResponse for TrustedServerError { Self::InvalidHeaderValue { .. } => StatusCode::BAD_REQUEST, Self::InvalidUtf8 { .. } => StatusCode::INTERNAL_SERVER_ERROR, Self::KvStore { .. } => StatusCode::SERVICE_UNAVAILABLE, + Self::ConfigStoreUnavailable { .. } => StatusCode::SERVICE_UNAVAILABLE, Self::Prebid { .. } => StatusCode::BAD_GATEWAY, Self::Integration { .. } => StatusCode::BAD_GATEWAY, Self::Proxy { .. } => StatusCode::BAD_GATEWAY, @@ -211,6 +217,13 @@ mod tests { }, StatusCode::SERVICE_UNAVAILABLE, ), + ( + TrustedServerError::ConfigStoreUnavailable { + store_name: String::from("app_config"), + message: String::from("store unavailable"), + }, + StatusCode::SERVICE_UNAVAILABLE, + ), ( TrustedServerError::Prebid { message: String::from("adapter error"), @@ -286,6 +299,9 @@ mod tests { TrustedServerError::RequestTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE, TrustedServerError::InvalidHeaderValue { .. } => StatusCode::BAD_REQUEST, TrustedServerError::KvStore { .. } => StatusCode::SERVICE_UNAVAILABLE, + TrustedServerError::ConfigStoreUnavailable { .. } => { + StatusCode::SERVICE_UNAVAILABLE + } TrustedServerError::Prebid { .. } => StatusCode::BAD_GATEWAY, TrustedServerError::Integration { .. } => StatusCode::BAD_GATEWAY, TrustedServerError::Proxy { .. } => StatusCode::BAD_GATEWAY, @@ -400,6 +416,25 @@ mod tests { assert_eq!(error.user_message(), "Invalid header value"); } + #[test] + fn config_store_unavailable_maps_to_503() { + let error = TrustedServerError::ConfigStoreUnavailable { + store_name: String::from("app_config"), + message: String::from("unavailable or not seeded"), + }; + assert_eq!( + error.status_code(), + StatusCode::SERVICE_UNAVAILABLE, + "config-store read failure should map to 503" + ); + // Detail stays server-side; the public body is the generic catch-all. + assert_eq!( + error.user_message(), + "An internal error occurred", + "503 client body must not leak internal config-store detail" + ); + } + #[test] fn status_code_maps_each_error_variant_to_expected_http_response() { // Compile-time guard: adding a TrustedServerError variant without @@ -413,6 +448,7 @@ mod tests { | TrustedServerError::InvalidUtf8 { .. } | TrustedServerError::InvalidHeaderValue { .. } | TrustedServerError::KvStore { .. } + | TrustedServerError::ConfigStoreUnavailable { .. } | TrustedServerError::Prebid { .. } | TrustedServerError::Integration { .. } | TrustedServerError::Proxy { .. } @@ -499,6 +535,13 @@ mod tests { }, StatusCode::SERVICE_UNAVAILABLE, ), + ( + TrustedServerError::ConfigStoreUnavailable { + store_name: "app_config".to_string(), + message: "config store unavailable".to_string(), + }, + StatusCode::SERVICE_UNAVAILABLE, + ), ( TrustedServerError::Auction { message: "auction failed".to_string(), From 4d6509f6b45497a86018b17bfc61d1daa0067168 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Sat, 27 Jun 2026 22:18:52 +0530 Subject: [PATCH 2/4] Classify config-store read failures as ConfigStoreUnavailable (503) Reads (blob key + each chunk) map to ConfigStoreUnavailable (503); envelope/ chunk verification and settings validation stay Configuration (500). Covers the blob/chunk load model on the updated ts-cli-audit base. --- .../trusted-server-core/src/settings_data.rs | 148 ++++++++++++++++-- 1 file changed, 135 insertions(+), 13 deletions(-) diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index bdf46a849..4ad7c9b11 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -36,9 +36,12 @@ struct FastlyChunkRef { /// /// # Errors /// -/// Returns [`TrustedServerError::Configuration`] when the config blob is -/// missing, cannot be read, fails envelope verification, or fails Trusted -/// Server settings validation. +/// Returns [`TrustedServerError::ConfigStoreUnavailable`] (HTTP 503) when the +/// config blob (or a referenced chunk) cannot be read — store unseeded, +/// transient backend, or a chunk key missing. Returns +/// [`TrustedServerError::Configuration`] (HTTP 500) when the read succeeds but +/// the blob fails envelope/chunk verification or Trusted Server settings +/// validation. pub fn get_settings_from_services( services: &RuntimeServices, ) -> Result> { @@ -66,9 +69,10 @@ pub fn default_config_key() -> String { /// /// # Errors /// -/// Returns [`TrustedServerError::Configuration`] when the config blob is -/// missing, cannot be read, fails envelope verification, or fails Trusted -/// Server settings validation. +/// Returns [`TrustedServerError::ConfigStoreUnavailable`] (HTTP 503) when the +/// config blob (or a referenced chunk) cannot be read, and +/// [`TrustedServerError::Configuration`] (HTTP 500) when the read succeeds but +/// envelope/chunk verification or settings validation fails. pub fn get_settings_from_config_store( config_store: &dyn PlatformConfigStore, store_name: &StoreName, @@ -84,12 +88,14 @@ fn read_config_entry( store_name: &StoreName, key: &str, ) -> Result> { - let message = format!( - "failed to read Trusted Server app config key `{key}` from config store `{store_name}`" - ); config_store .get(store_name, key) - .change_context(TrustedServerError::Configuration { message }) + .change_context(TrustedServerError::ConfigStoreUnavailable { + store_name: store_name.to_string(), + message: format!( + "unavailable or not seeded (failed to read `{key}`) — run `ts config push`" + ), + }) } fn resolve_fastly_chunk_pointer( @@ -160,6 +166,7 @@ fn configuration_error(message: String) -> Result Date: Sat, 27 Jun 2026 22:18:52 +0530 Subject: [PATCH 3/4] Lock adapter 503 response for ConfigStoreUnavailable --- .../src/error.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/trusted-server-adapter-fastly/src/error.rs b/crates/trusted-server-adapter-fastly/src/error.rs index 560a2e181..f3182ddc8 100644 --- a/crates/trusted-server-adapter-fastly/src/error.rs +++ b/crates/trusted-server-adapter-fastly/src/error.rs @@ -17,3 +17,26 @@ pub fn to_error_response(report: &Report) -> Response { Response::from_status(root_error.status_code()) .with_body_text_plain(&format!("{}\n", root_error.user_message())) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_store_unavailable_renders_503() { + // Locks the end-to-end mapping: a config-store read failure reaches the + // client as 503 via `status_code()` — not bypassed by the adapter. + let report = Report::new(TrustedServerError::ConfigStoreUnavailable { + store_name: "app_config".to_string(), + message: "unavailable or not seeded".to_string(), + }); + + let response = to_error_response(&report); + + assert_eq!( + response.get_status(), + fastly::http::StatusCode::SERVICE_UNAVAILABLE, + "config-store read failure should render as 503 to the client" + ); + } +} From 854edef34c488b2c73df62dc80ba0358bd8c7d6c Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Sat, 27 Jun 2026 22:18:52 +0530 Subject: [PATCH 4/4] Add HTTP-layer config-store 503 design doc --- ...6-06-27-edgezero-http-config-503-design.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-27-edgezero-http-config-503-design.md diff --git a/docs/superpowers/specs/2026-06-27-edgezero-http-config-503-design.md b/docs/superpowers/specs/2026-06-27-edgezero-http-config-503-design.md new file mode 100644 index 000000000..2097e2846 --- /dev/null +++ b/docs/superpowers/specs/2026-06-27-edgezero-http-config-503-design.md @@ -0,0 +1,78 @@ +# Design: HTTP-Layer Config-Store Load Hardening (503) + +- **Date:** 2026-06-27 +- **Author:** Prakash (HTTP-layer / runtime). +- **Status:** implemented on `feature/edgezero-269-http` (stacked on `feature/ts-cli-audit`). +- **Supersedes:** the earlier per-key flatten/hash variant of this design. The + CLI now stores Trusted Server config as a single **blob** (optionally chunked + for Fastly value-size limits — `config_payload::settings_from_config_blob`, + `settings_data::FastlyChunkPointer`), so the load path and this spec are + re-derived against that model. + +--- + +## 1. Problem + +The runtime rebuilds `Settings` at boot by reading the `app_config` config +store. Before this change every read failure — including an **unseeded** store — +mapped to `TrustedServerError::Configuration` → **500**, indistinguishable from a +genuine code bug. `trusted-server.toml` is deleted, so an unseeded store is an +expected operational state (fresh install, or cutover before `ts config push`), +not a bug. + +## 2. Load sequence (blob model) + +`crates/trusted-server-core/src/settings_data.rs`: + +``` +get_settings_from_services + → get_settings_from_config_store(store, name, key) + → read_config_entry(key) // READ — the blob value + → resolve_fastly_chunk_pointer(value) // if a chunk pointer: + → read_config_entry(chunk.key) × N // READ — each chunk + → verify chunk len + sha, envelope len + sha // VERIFY + → settings_from_config_blob(envelope_json) // VERIFY — parse + validate +``` + +`read_config_entry` is the **single read seam** (used for both the top-level +blob key and every chunk key). `key` resolves via +`EnvConfig::store_key("config", "app_config")`. + +## 3. Behavior matrix (the contract) + +The boundary is **"couldn't read the config"** vs **"read it but it's +invalid"** — classified by **call site**, because `PlatformConfigStore::get` +collapses key-absent and transport failure into one `Err` (see §5). + +| Situation | Where | Status | +| ---------------------------------------------------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------- | +| Blob key or a referenced chunk cannot be read (store unseeded, transient backend, chunk key missing) | `read_config_entry` | **503** `ConfigStoreUnavailable`, actionable hint `run \`ts config push\`` | +| Chunk len/sha mismatch, envelope len/sha mismatch, unsupported pointer version | `resolve_fastly_chunk_pointer` verify | **500** `Configuration` | +| Blob read OK but not a valid envelope / settings invalid | `settings_from_config_blob` | **500** `Configuration` | +| Seeded + valid | — | `Settings` loads | + +503 is correct for the read column: unseeded → seed it; transient → retry. + +## 4. Mechanism (one new variant) + +`TrustedServerError::ConfigStoreUnavailable { store_name, message }` → +`StatusCode::SERVICE_UNAVAILABLE` (precedent: the existing `KvStore` 503 arm). +Only `read_config_entry`'s `change_context` target changes from `Configuration` +to the new variant; all verify/parse paths are untouched. No `PlatformError` +or `PlatformConfigStore` change. + +**Security:** the actionable hint rides the error chain (`Display`) to the +**server log** only. The public 503 body stays the generic +`user_message()` catch-all — no `user_message()` arm is added for this variant — +so internal tooling/paths never leak to clients. + +## 5. Out of scope / follow-up + +- `PlatformConfigStore::get → Result>` (absence as a value, not an + error) would let the runtime distinguish **unseeded** (`Ok(None)`) from + **transient** (`Err`) precisely instead of classifying by call site. It is the + store-convergence direction (edgezero's own `ConfigStore::get` shape) and + touches every impl + caller across request-signing and DataDome — tracked as a + separate, cross-cutting change, not this PR. +- Non-Fastly adapter (cloudflare/spin/axum) parity rides the EdgeZero adapter + stack.