Skip to content

Commit ede1c06

Browse files
njbrakeclaude
andauthored
fix(gateway): raise error for unresolved env var references in config (#838)
## Description `_resolve_env_vars()` uses `os.getenv(env_var, config)` as fallback, meaning if `$MASTER_KEY` is not set, the literal string `${MASTER_KEY}` is silently used as the actual secret. The gateway starts with placeholder strings as real credentials. This PR raises a `ValueError` at startup when a referenced env var is missing, failing fast with a clear error message. ## PR Type - 🐛 Bug Fix ## Checklist - [x] I understand the code I am submitting. - [x] I have added unit tests that prove my fix/feature works - [x] I have run this code locally and verified it fixes the issue. - [x] New and existing tests pass locally - [ ] Documentation was updated where necessary - [x] I have read and followed the [contribution guidelines](https://github.com/mozilla-ai/any-llm/blob/main/CONTRIBUTING.md) - **AI Usage:** - [ ] No AI was used. - [ ] AI was used for drafting/refactoring. - [x] This is fully AI-generated. ## AI Usage Information - AI Model used: Claude Opus 4.6 - AI Developer Tool used: Claude Code - Any other info you'd like to share: Identified during a comprehensive gateway code review. - [x] I am an AI Agent filling out this form (check box if true) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a1792e1 commit ede1c06

File tree

2 files changed

+66
-1
lines changed

2 files changed

+66
-1
lines changed

src/any_llm/gateway/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,20 @@ def _resolve_env_vars(config: dict[str, Any]) -> dict[str, Any]:
7474
"""Recursively resolve environment variable references in config.
7575
7676
Supports ${VAR_NAME} syntax in string values.
77+
78+
Raises:
79+
ValueError: If an environment variable reference cannot be resolved
80+
7781
"""
7882
if isinstance(config, dict):
7983
return {key: _resolve_env_vars(value) for key, value in config.items()}
8084
if isinstance(config, list):
8185
return [_resolve_env_vars(item) for item in config]
8286
if isinstance(config, str) and config.startswith("${") and config.endswith("}"):
8387
env_var = config[2:-1]
84-
return os.getenv(env_var, config)
88+
value = os.getenv(env_var)
89+
if value is None:
90+
msg = f"Environment variable '{env_var}' is not set (referenced in config as '${{{env_var}}}')"
91+
raise ValueError(msg)
92+
return value
8593
return config
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Tests for environment variable resolution in config."""
2+
3+
import os
4+
5+
import pytest
6+
7+
from any_llm.gateway.config import _resolve_env_vars
8+
9+
10+
def test_resolve_existing_env_var() -> None:
11+
"""Test that existing env vars are resolved."""
12+
os.environ["TEST_RESOLVE_VAR"] = "resolved_value"
13+
try:
14+
result = _resolve_env_vars({"key": "${TEST_RESOLVE_VAR}"})
15+
assert result["key"] == "resolved_value"
16+
finally:
17+
del os.environ["TEST_RESOLVE_VAR"]
18+
19+
20+
def test_resolve_missing_env_var_raises() -> None:
21+
"""Test that missing env vars raise ValueError instead of using placeholder."""
22+
# Ensure the var does not exist
23+
os.environ.pop("DEFINITELY_MISSING_VAR", None)
24+
with pytest.raises(ValueError, match="DEFINITELY_MISSING_VAR"):
25+
_resolve_env_vars({"key": "${DEFINITELY_MISSING_VAR}"})
26+
27+
28+
def test_resolve_nested_dict() -> None:
29+
"""Test that env vars are resolved recursively in nested dicts."""
30+
os.environ["TEST_NESTED_VAR"] = "nested_value"
31+
try:
32+
result = _resolve_env_vars({"outer": {"inner": "${TEST_NESTED_VAR}"}})
33+
assert result["outer"]["inner"] == "nested_value"
34+
finally:
35+
del os.environ["TEST_NESTED_VAR"]
36+
37+
38+
def test_resolve_list_values() -> None:
39+
"""Test that env vars are resolved in list values."""
40+
os.environ["TEST_LIST_VAR"] = "list_value"
41+
try:
42+
result = _resolve_env_vars({"items": ["${TEST_LIST_VAR}", "literal"]})
43+
assert result["items"] == ["list_value", "literal"]
44+
finally:
45+
del os.environ["TEST_LIST_VAR"]
46+
47+
48+
def test_non_env_var_strings_pass_through() -> None:
49+
"""Test that strings not matching ${...} pattern pass through unchanged."""
50+
result = _resolve_env_vars({"key": "just a string"})
51+
assert result["key"] == "just a string"
52+
53+
54+
def test_partial_env_var_syntax_passes_through() -> None:
55+
"""Test that partial env var syntax (not matching ${...}) passes through."""
56+
result = _resolve_env_vars({"key": "${PARTIAL"})
57+
assert result["key"] == "${PARTIAL"

0 commit comments

Comments
 (0)