Skip to content
Merged
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
71 changes: 70 additions & 1 deletion crates/common/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ pub struct Settings {
#[serde(default, deserialize_with = "vec_from_seq_or_map")]
#[validate(nested)]
pub handlers: Vec<Handler>,
#[serde(default)]
#[serde(default, deserialize_with = "map_from_obj_or_str")]
pub response_headers: HashMap<String, String>,
pub request_signing: Option<RequestSigning>,
#[serde(default)]
Expand Down Expand Up @@ -423,6 +423,48 @@ fn validate_path(value: &str) -> Result<(), ValidationError> {
// This lets env vars like TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS__0=smartadserver work, which the config env source
// represents as an object {"0": "value"} rather than a sequence. Also supports string inputs that are
// JSON arrays or comma-separated values.
/// Deserializes a `HashMap<String, String>` from either:
/// - A TOML table / JSON object (standard deserialization)
/// - A JSON string (e.g. from env var: `'{"Key": "value"}'`)
///
/// This allows setting map fields via environment variables while
/// preserving key casing and special characters like hyphens.
pub(crate) fn map_from_obj_or_str<'de, D>(
deserializer: D,
) -> Result<HashMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
let v = JsonValue::deserialize(deserializer)?;
match v {
JsonValue::Object(map) => map
.into_iter()
.map(|(k, v)| {
let val = match v {
JsonValue::String(s) => s,
other => other.to_string(),
};
Ok((k, val))
})
.collect(),
JsonValue::String(s) => {
let txt = s.trim();
if txt.starts_with('{') {
serde_json::from_str::<HashMap<String, String>>(txt)
.map_err(serde::de::Error::custom)
} else {
Err(serde::de::Error::custom(
"expected JSON object string, e.g. '{\"Key\": \"value\"}'",
))
}
}
JsonValue::Null => Ok(HashMap::new()),
other => Err(serde::de::Error::custom(format!(
"expected object or JSON string, got {other}",
))),
}
}

pub(crate) fn vec_from_seq_or_map<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
Expand Down Expand Up @@ -792,6 +834,33 @@ mod tests {
);
}

#[test]
fn test_response_headers_override_with_json_env() {
let toml_str = crate_test_settings_str();
let env_key = format!(
"{}{}RESPONSE_HEADERS",
ENVIRONMENT_VARIABLE_PREFIX, ENVIRONMENT_VARIABLE_SEPARATOR,
);

temp_env::with_var(
env_key,
Some(r#"{"X-Robots-Tag": "noindex", "X-Custom-Header": "custom value"}"#),
|| {
let settings = Settings::from_toml(&toml_str)
.expect("Settings should parse with JSON response_headers env");
assert_eq!(settings.response_headers.len(), 2);
assert_eq!(
settings.response_headers.get("X-Robots-Tag"),
Some(&"noindex".to_string())
);
assert_eq!(
settings.response_headers.get("X-Custom-Header"),
Some(&"custom value".to_string())
);
},
);
}

#[test]
fn test_settings_extra_fields() {
let toml_str = crate_test_settings_str() + "\nhello = 1";
Expand Down
8 changes: 7 additions & 1 deletion docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,16 @@ Cache-Control = "public, max-age=3600"

**Environment Override**:

Use a JSON object to preserve header name casing and hyphens:

```bash
TRUSTED_SERVER__RESPONSE_HEADERS__X_CUSTOM_HEADER="custom value"
TRUSTED_SERVER__RESPONSE_HEADERS='{"X-Robots-Tag": "noindex", "X-Custom-Header": "custom value"}'
```

::: tip Why JSON?
Individual env var keys like `TRUSTED_SERVER__RESPONSE_HEADERS__X_CUSTOM_HEADER` lose hyphens and casing (becoming `x_custom_header`). The JSON format preserves exact header names.
:::

**Use Cases**:

- Custom tracking headers
Expand Down
6 changes: 4 additions & 2 deletions trusted-server.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ template = "{{ client_ip }}:{{ user_agent }}:{{ accept_language }}:{{ accept_enc
# Allows publishers to include tags such as X-Robots-Tag: noindex
# [response_headers]
# X-Custom-Header = "custom header value"
#
# Or via environment variable (JSON preserves header name casing and hyphens):
# TRUSTED_SERVER__RESPONSE_HEADERS='{"X-Robots-Tag": "noindex", "X-Custom-Header": "custom value"}'

# Request Signing Configuration
# Enable signing of OpenRTB requests and other API calls
Expand All @@ -38,10 +41,9 @@ enabled = true
server_url = "http://68.183.113.79:8000"
timeout_ms = 1000
bidders = ["kargo", "rubicon", "appnexus", "openx"]
auto_configure = false
debug = false
# debug_query_params = ""
# script_handler = "/prebid.js"
# script_patterns = ["/prebid.js"]

[integrations.nextjs]
enabled = false
Expand Down