@@ -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+
426468pub ( crate ) fn vec_from_seq_or_map < ' de , D , T > ( deserializer : D ) -> Result < Vec < T > , D :: Error >
427469where
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 ( ) + "\n hello = 1" ;
0 commit comments