Skip to content

Commit 775dc72

Browse files
authored
Support response headers via JSON environment variable (#318)
* Fix response header settings * Improved error message and test
1 parent 0f032a1 commit 775dc72

3 files changed

Lines changed: 81 additions & 4 deletions

File tree

crates/common/src/settings.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ pub struct Settings {
310310
#[serde(default, deserialize_with = "vec_from_seq_or_map")]
311311
#[validate(nested)]
312312
pub handlers: Vec<Handler>,
313-
#[serde(default)]
313+
#[serde(default, deserialize_with = "map_from_obj_or_str")]
314314
pub response_headers: HashMap<String, String>,
315315
pub request_signing: Option<RequestSigning>,
316316
#[serde(default)]
@@ -423,6 +423,48 @@ fn validate_path(value: &str) -> Result<(), ValidationError> {
423423
// This lets env vars like TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS__0=smartadserver work, which the config env source
424424
// represents as an object {"0": "value"} rather than a sequence. Also supports string inputs that are
425425
// JSON arrays or comma-separated values.
426+
/// Deserializes a `HashMap<String, String>` from either:
427+
/// - A TOML table / JSON object (standard deserialization)
428+
/// - A JSON string (e.g. from env var: `'{"Key": "value"}'`)
429+
///
430+
/// This allows setting map fields via environment variables while
431+
/// preserving key casing and special characters like hyphens.
432+
pub(crate) fn map_from_obj_or_str<'de, D>(
433+
deserializer: D,
434+
) -> Result<HashMap<String, String>, D::Error>
435+
where
436+
D: Deserializer<'de>,
437+
{
438+
let v = JsonValue::deserialize(deserializer)?;
439+
match v {
440+
JsonValue::Object(map) => map
441+
.into_iter()
442+
.map(|(k, v)| {
443+
let val = match v {
444+
JsonValue::String(s) => s,
445+
other => other.to_string(),
446+
};
447+
Ok((k, val))
448+
})
449+
.collect(),
450+
JsonValue::String(s) => {
451+
let txt = s.trim();
452+
if txt.starts_with('{') {
453+
serde_json::from_str::<HashMap<String, String>>(txt)
454+
.map_err(serde::de::Error::custom)
455+
} else {
456+
Err(serde::de::Error::custom(
457+
"expected JSON object string, e.g. '{\"Key\": \"value\"}'",
458+
))
459+
}
460+
}
461+
JsonValue::Null => Ok(HashMap::new()),
462+
other => Err(serde::de::Error::custom(format!(
463+
"expected object or JSON string, got {other}",
464+
))),
465+
}
466+
}
467+
426468
pub(crate) fn vec_from_seq_or_map<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
427469
where
428470
D: Deserializer<'de>,
@@ -792,6 +834,33 @@ mod tests {
792834
);
793835
}
794836

837+
#[test]
838+
fn test_response_headers_override_with_json_env() {
839+
let toml_str = crate_test_settings_str();
840+
let env_key = format!(
841+
"{}{}RESPONSE_HEADERS",
842+
ENVIRONMENT_VARIABLE_PREFIX, ENVIRONMENT_VARIABLE_SEPARATOR,
843+
);
844+
845+
temp_env::with_var(
846+
env_key,
847+
Some(r#"{"X-Robots-Tag": "noindex", "X-Custom-Header": "custom value"}"#),
848+
|| {
849+
let settings = Settings::from_toml(&toml_str)
850+
.expect("Settings should parse with JSON response_headers env");
851+
assert_eq!(settings.response_headers.len(), 2);
852+
assert_eq!(
853+
settings.response_headers.get("X-Robots-Tag"),
854+
Some(&"noindex".to_string())
855+
);
856+
assert_eq!(
857+
settings.response_headers.get("X-Custom-Header"),
858+
Some(&"custom value".to_string())
859+
);
860+
},
861+
);
862+
}
863+
795864
#[test]
796865
fn test_settings_extra_fields() {
797866
let toml_str = crate_test_settings_str() + "\nhello = 1";

docs/guide/configuration.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,10 +434,16 @@ Cache-Control = "public, max-age=3600"
434434

435435
**Environment Override**:
436436

437+
Use a JSON object to preserve header name casing and hyphens:
438+
437439
```bash
438-
TRUSTED_SERVER__RESPONSE_HEADERS__X_CUSTOM_HEADER="custom value"
440+
TRUSTED_SERVER__RESPONSE_HEADERS='{"X-Robots-Tag": "noindex", "X-Custom-Header": "custom value"}'
439441
```
440442

443+
::: tip Why JSON?
444+
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.
445+
:::
446+
441447
**Use Cases**:
442448

443449
- Custom tracking headers

trusted-server.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ template = "{{ client_ip }}:{{ user_agent }}:{{ accept_language }}:{{ accept_enc
2525
# Allows publishers to include tags such as X-Robots-Tag: noindex
2626
# [response_headers]
2727
# X-Custom-Header = "custom header value"
28+
#
29+
# Or via environment variable (JSON preserves header name casing and hyphens):
30+
# TRUSTED_SERVER__RESPONSE_HEADERS='{"X-Robots-Tag": "noindex", "X-Custom-Header": "custom value"}'
2831

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

4648
[integrations.nextjs]
4749
enabled = false

0 commit comments

Comments
 (0)