From 421ae5f42bbc05fe7412f932a4ae76df07072373 Mon Sep 17 00:00:00 2001 From: manish-wekan Date: Fri, 16 Oct 2026 05:26:54 +0530 Subject: [PATCH] ConfigService: process environment wins over .env .env values were written over os.environ unconditionally, so the PORT=3000 that `nitrostack init` puts in .env overrode the PORT a host passes when starting the server. NitroStudio starts a second project on 3001; it bound 3000 anyway and died with "address already in use" (NITSTD-222). Keys already in the process environment are now kept, as with python-dotenv and Node's dotenv. .env still fills keys the process doesn't set, and defaults stay lowest. Co-Authored-By: Claude Opus 5.5 --- nitrostack/auth/config.py | 7 ++++++- tests/test_auth_modules.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/nitrostack/auth/config.py b/nitrostack/auth/config.py index f97c56d..7b4cd4c 100644 --- a/nitrostack/auth/config.py +++ b/nitrostack/auth/config.py @@ -19,7 +19,10 @@ def __init__( # Merge system environment variables first self._config.update(os.environ) - # Parse .env manually (no stdout prints allowed) + # Parse .env manually (no stdout prints allowed). The process + # environment wins, as with python-dotenv and Node's dotenv: a host + # that starts the server with PORT=3001 must not be overridden by the + # PORT=3000 that `nitrostack init` writes into .env. if not ignore_env_file and os.path.exists(env_file_path): try: with open(env_file_path, "r", encoding="utf-8") as f: @@ -31,6 +34,8 @@ def __init__( k, v = line.split("=", 1) k = k.strip() v = v.strip().strip("'\"") # Remove wrapping quotes + if k in os.environ: + continue self._config[k] = v os.environ[k] = v except Exception as e: diff --git a/tests/test_auth_modules.py b/tests/test_auth_modules.py index cd19843..8030511 100644 --- a/tests/test_auth_modules.py +++ b/tests/test_auth_modules.py @@ -178,3 +178,33 @@ def test_api_key_empty_string_is_rejected(monkeypatch): service = DIContainer.get_instance().resolve(ApiKeyService) assert service.validate("") is False assert service.validate("anything") is False + + +def test_config_process_env_wins_over_env_file(tmp_path, monkeypatch): + from nitrostack.core.app import resolve_http_port + + env_file = tmp_path / ".env" + env_file.write_text("PORT=3000\nFROM_FILE=file\n", encoding="utf-8") + monkeypatch.setenv("PORT", "3001") + monkeypatch.delenv("FROM_FILE", raising=False) + ConfigModule.for_root(env_file_path=str(env_file), defaults={"PORT": "9999", "ONLY_DEFAULT": "d"}) + cfg = DIContainer.get_instance().resolve(ConfigService) + assert cfg.get("PORT") == "3001" + assert resolve_http_port() == 3001 + # Keys the process doesn't set still come from the file, then defaults. + assert cfg.get("FROM_FILE") == "file" + assert cfg.get("ONLY_DEFAULT") == "d" + monkeypatch.delenv("FROM_FILE", raising=False) + + +def test_config_env_file_port_applies_when_process_has_none(tmp_path, monkeypatch): + from nitrostack.core.app import resolve_http_port + + env_file = tmp_path / ".env" + env_file.write_text("PORT=3005\n", encoding="utf-8") + monkeypatch.delenv("PORT", raising=False) + ConfigModule.for_root(env_file_path=str(env_file), defaults={"PORT": "3000"}) + cfg = DIContainer.get_instance().resolve(ConfigService) + assert cfg.get("PORT") == "3005" + assert resolve_http_port() == 3005 + monkeypatch.delenv("PORT", raising=False)