Skip to content
Draft
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
19 changes: 19 additions & 0 deletions crates/trusted-server-adapter-fastly/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,22 @@ pub fn to_error_response(report: &Report<TrustedServerError>) -> 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() {
let report = Report::new(TrustedServerError::ConfigStoreUnavailable {
store_name: "app_config".to_string(),
message: "not seeded".to_string(),
});
let resp = to_error_response(&report);
assert_eq!(
resp.get_status(),
fastly::http::StatusCode::SERVICE_UNAVAILABLE,
"should render 503 for ConfigStoreUnavailable"
);
}
}
23 changes: 23 additions & 0 deletions crates/trusted-server-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -242,6 +248,15 @@ mod tests {
assert_eq!(error.user_message(), "Invalid header value");
}

#[test]
fn config_store_unavailable_maps_to_503() {
let err = TrustedServerError::ConfigStoreUnavailable {
store_name: "app_config".to_string(),
message: "not seeded".to_string(),
};
assert_eq!(err.status_code(), StatusCode::SERVICE_UNAVAILABLE);
}

#[test]
fn status_code_maps_each_error_variant_to_expected_http_response() {
// Compile-time guard: adding a TrustedServerError variant without
Expand All @@ -264,6 +279,7 @@ mod tests {
| TrustedServerError::EdgeCookie { .. }
| TrustedServerError::PartnerNotFound { .. }
| TrustedServerError::RequestTooLarge { .. }
| TrustedServerError::ConfigStoreUnavailable { .. }
| TrustedServerError::InsecureDefault { .. } => (),
};

Expand Down Expand Up @@ -341,6 +357,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(),
Expand Down
90 changes: 81 additions & 9 deletions crates/trusted-server-core/src/settings_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ const DEFAULT_CONFIG_STORE_ID: &str = "app_config";
///
/// # Errors
///
/// Returns [`TrustedServerError::Configuration`] when metadata or any flattened
/// config entry is missing, cannot be read, fails hash verification, or fails
/// Trusted Server settings validation.
/// Returns [`TrustedServerError::ConfigStoreUnavailable`] (HTTP 503) when a
/// config-store entry cannot be read (store unseeded, transient backend, or a
/// listed key missing), and [`TrustedServerError::Configuration`] /
/// [`TrustedServerError::Settings`] (HTTP 500) when the read succeeds but
/// reconstruction fails (metadata unparseable, hash verification, or settings
/// validation).
pub fn get_settings_from_services(
services: &RuntimeServices,
) -> Result<Settings, Report<TrustedServerError>> {
Expand All @@ -32,9 +35,12 @@ pub fn get_settings_from_services(
///
/// # Errors
///
/// Returns [`TrustedServerError::Configuration`] when metadata or any flattened
/// config entry is missing, cannot be read, fails hash verification, or fails
/// Trusted Server settings validation.
/// Returns [`TrustedServerError::ConfigStoreUnavailable`] (HTTP 503) when a
/// config-store entry cannot be read (store unseeded, transient backend, or a
/// listed key missing), and [`TrustedServerError::Configuration`] /
/// [`TrustedServerError::Settings`] (HTTP 500) when the read succeeds but
/// reconstruction fails (metadata unparseable, hash verification, or settings
/// validation).
pub fn get_settings_from_config_store(
config_store: &dyn PlatformConfigStore,
store_name: &StoreName,
Expand Down Expand Up @@ -66,17 +72,19 @@ fn read_config_entry(
) -> Result<String, Report<TrustedServerError>> {
config_store
.get(store_name, key)
.change_context(TrustedServerError::Configuration {
.change_context(TrustedServerError::ConfigStoreUnavailable {
store_name: store_name.to_string(),
message: format!(
"failed to read Trusted Server app config key `{key}` from config store `{store_name}`"
),
"unavailable or not seeded (failed to read `{key}`) — run `ts config push`"
),
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config_payload::build_config_payload;
use crate::error::IntoHttpResponse;
use crate::platform::PlatformError;
use crate::settings::Settings;
use crate::test_support::tests::crate_test_settings_str;
Expand Down Expand Up @@ -142,4 +150,68 @@ mod tests {
"error should mention missing keys metadata"
);
}

#[test]
fn unseeded_store_is_config_store_unavailable_503() {
let store = MemoryConfigStore {
entries: BTreeMap::new(),
};
let err = get_settings_from_config_store(&store, &StoreName::from("app_config"))
.expect_err("unseeded store must error");
assert_eq!(
err.current_context().status_code(),
http::StatusCode::SERVICE_UNAVAILABLE,
"unseeded store read failure should map to 503"
);
// The actionable hint must ride the error chain so it reaches the
// server log (the operator's channel); the public 503 body stays
// generic by design.
assert!(
format!("{err:?}").contains("ts config push"),
"error chain should carry the actionable `ts config push` hint for logs"
);
}

#[test]
fn malformed_hash_stays_500() {
let mut entries = build_config_payload(
&Settings::from_toml(&crate_test_settings_str()).expect("should parse"),
)
.expect("should build payload")
.entries;
entries.insert(CONFIG_HASH_KEY.to_string(), "sha256:deadbeef".to_string());
let store = MemoryConfigStore { entries };
let err = get_settings_from_config_store(&store, &StoreName::from("app_config"))
.expect_err("hash mismatch must error");
assert_eq!(
err.current_context().status_code(),
http::StatusCode::INTERNAL_SERVER_ERROR,
"reconstruct/verify failure should stay 500"
);
}

#[test]
fn missing_listed_key_is_503() {
// Metadata (`ts-config-keys` / `ts-config-hash`) reads succeed, but a key
// the metadata lists is absent — still a read failure → 503.
let mut entries = build_config_payload(
&Settings::from_toml(&crate_test_settings_str()).expect("should parse"),
)
.expect("should build payload")
.entries;
let victim = entries
.keys()
.find(|key| !key.starts_with("ts-config-"))
.cloned()
.expect("payload should have at least one settings key");
entries.remove(&victim);
let store = MemoryConfigStore { entries };
let err = get_settings_from_config_store(&store, &StoreName::from("app_config"))
.expect_err("missing listed key must error");
assert_eq!(
err.current_context().status_code(),
http::StatusCode::SERVICE_UNAVAILABLE,
"a listed key missing is a config-store read failure → 503"
);
}
}
Loading
Loading