[Feature]: Fail-fast, centralized configuration validation for the Flask ML API
Summary
The Flask ML API reads configuration ad-hoc via scattered os.getenv(...) calls across backend/api.py, backend/rate_limiting.py and backend/domain_checker.py. A backend/config.py module exists but api.py doesn't use it (it only serves the FastAPI/DB side). The result:
- Misconfiguration fails late and partially —
INTERNAL_SECRET is validated at import, but MODEL_PATH/VECTORIZER_PATH, MAX_MESSAGE_LENGTH, SERVICE_IP_ALLOWLIST, FLASK_HOST/FLASK_DEBUG, rate-limit windows and optional threat-intel keys fail lazily (or silently fall back), so an operator finds out on the first request, not at boot.
- Defaults and parsing (
_env_flag, int(os.getenv(...))) are duplicated in more than one file.
- Errors are reported one at a time — fix one env var, restart, hit the next.
This proposes a single validated Settings object, loaded once at startup, that reports all configuration problems together.
Motivation / current state
Confirmed scattered reads include (non-exhaustive):
api.py: INTERNAL_SECRET, MODEL_PATH, VECTORIZER_PATH, LABEL_ENCODER_PATH, URL_MODEL_PATH, URL_VECTORIZER_PATH, MAX_MESSAGE_LENGTH, NODE_ENV, SERVICE_IP_ALLOWLIST, FLASK_PORT, FLASK_HOST, FLASK_DEBUG.
rate_limiting.py: REDIS_URL / RATE_LIMIT_STORAGE_URI, per-policy *_RATE_LIMIT overrides.
domain_checker.py: SAFE_BROWSING_API_KEY, VIRUSTOTAL_API_KEY.
backend/config.py today is 14 lines and only covers the DB/FastAPI vars — it is not the ML API's config source. Not a duplicate of any open issue.
Proposed solution
Add a Settings dataclass that owns parsing, defaults and validation, and swap the scattered reads for settings.*. Validation collects every error and raises one ConfigError listing all of them (matches the "report all bad inputs, not just the first" principle).
| File |
Change |
backend/settings.py (new) |
@dataclass(frozen=True) Settings (typed fields for paths, host/port, flags, limits, IP allowlist, optional API keys) + load_settings() that aggregates all validation errors (INTERNAL_SECRET presence/length, port range, non-negative MAX_MESSAGE_LENGTH, FLASK_DEBUG+non-loopback combo, model files exist) into one ConfigError; typed _env_int/_env_flag helpers |
backend/api.py |
replace scattered os.getenv + _load_internal_secret + _env_flag with settings = load_settings(); use settings.* at the model-load, IP-allowlist, message-length and __main__ sites |
backend/config.py |
re-export the ML fields from settings (single source of truth), keep DB fields |
backend/tests/test_settings.py (new) |
missing INTERNAL_SECRET; too-short secret; invalid port; the aggregated error message names all problems at once; valid config loads |
backend/.env.example / README.md |
document the validated variables in one table |
Difficulty: Hard
Consolidating configuration touches several modules, requires careful aggregation-style validation, a typed immutable settings object, and must preserve existing security behaviour and tests — depth across the service's startup path.
[Feature]: Fail-fast, centralized configuration validation for the Flask ML API
Summary
The Flask ML API reads configuration ad-hoc via scattered
os.getenv(...)calls acrossbackend/api.py,backend/rate_limiting.pyandbackend/domain_checker.py. Abackend/config.pymodule exists butapi.pydoesn't use it (it only serves the FastAPI/DB side). The result:INTERNAL_SECRETis validated at import, butMODEL_PATH/VECTORIZER_PATH,MAX_MESSAGE_LENGTH,SERVICE_IP_ALLOWLIST,FLASK_HOST/FLASK_DEBUG, rate-limit windows and optional threat-intel keys fail lazily (or silently fall back), so an operator finds out on the first request, not at boot._env_flag,int(os.getenv(...))) are duplicated in more than one file.This proposes a single validated
Settingsobject, loaded once at startup, that reports all configuration problems together.Motivation / current state
Confirmed scattered reads include (non-exhaustive):
api.py:INTERNAL_SECRET,MODEL_PATH,VECTORIZER_PATH,LABEL_ENCODER_PATH,URL_MODEL_PATH,URL_VECTORIZER_PATH,MAX_MESSAGE_LENGTH,NODE_ENV,SERVICE_IP_ALLOWLIST,FLASK_PORT,FLASK_HOST,FLASK_DEBUG.rate_limiting.py:REDIS_URL/RATE_LIMIT_STORAGE_URI, per-policy*_RATE_LIMIToverrides.domain_checker.py:SAFE_BROWSING_API_KEY,VIRUSTOTAL_API_KEY.backend/config.pytoday is 14 lines and only covers the DB/FastAPI vars — it is not the ML API's config source. Not a duplicate of any open issue.Proposed solution
Add a
Settingsdataclass that owns parsing, defaults and validation, and swap the scattered reads forsettings.*. Validation collects every error and raises oneConfigErrorlisting all of them (matches the "report all bad inputs, not just the first" principle).backend/settings.py(new)@dataclass(frozen=True) Settings(typed fields for paths, host/port, flags, limits, IP allowlist, optional API keys) +load_settings()that aggregates all validation errors (INTERNAL_SECRETpresence/length, port range, non-negativeMAX_MESSAGE_LENGTH,FLASK_DEBUG+non-loopback combo, model files exist) into oneConfigError; typed_env_int/_env_flaghelpersbackend/api.pyos.getenv+_load_internal_secret+_env_flagwithsettings = load_settings(); usesettings.*at the model-load, IP-allowlist, message-length and__main__sitesbackend/config.pysettings(single source of truth), keep DB fieldsbackend/tests/test_settings.py(new)INTERNAL_SECRET; too-short secret; invalid port; the aggregated error message names all problems at once; valid config loadsbackend/.env.example/README.mdDifficulty: Hard
Consolidating configuration touches several modules, requires careful aggregation-style validation, a typed immutable settings object, and must preserve existing security behaviour and tests — depth across the service's startup path.