From 6a13feaf8bc6c24c595b7099c7f3118033cb484d Mon Sep 17 00:00:00 2001 From: gaku Date: Fri, 12 Jun 2026 03:39:43 +0000 Subject: [PATCH] refactor(profiles): align profile cookie env var with HERMES_WEBUI_* naming The profile cookie has been configurable since #1756 via WEBUI_PROFILE_COOKIE_NAME, the lone WebUI env var missing the HERMES_WEBUI_ prefix shared by every other setting (e.g. HERMES_WEBUI_COOKIE_NAME from #3981). - Read HERMES_WEBUI_PROFILE_COOKIE_NAME first (canonical name) - Keep WEBUI_PROFILE_COOKIE_NAME as a deprecated alias so existing deployments are unaffected; behavior is unchanged, only the name is aligned - Warn once per process for the legacy name (this resolver runs on every request, so the deprecation log must not fire per-request) - Add resolution tests covering canonical, legacy, precedence, blank, and warn-once paths --- api/helpers.py | 29 ++++++++++++++++++++-- tests/test_issue803.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/api/helpers.py b/api/helpers.py index 544eb811..0bb13b66 100644 --- a/api/helpers.py +++ b/api/helpers.py @@ -495,11 +495,36 @@ def read_body(handler) -> dict: # ── Profile cookie helpers (issue #798) ───────────────────────────────────── PROFILE_COOKIE_NAME = 'hermes_profile' +_PROFILE_COOKIE_ENV = 'HERMES_WEBUI_PROFILE_COOKIE_NAME' +_LEGACY_PROFILE_COOKIE_ENV = 'WEBUI_PROFILE_COOKIE_NAME' +_legacy_profile_cookie_warned = False def get_profile_cookie_name() -> str: - """Return the cookie name used to persist the active WebUI profile.""" - return os.getenv('WEBUI_PROFILE_COOKIE_NAME', PROFILE_COOKIE_NAME) + """Return the cookie name used to persist the active WebUI profile. + + Honours ``HERMES_WEBUI_PROFILE_COOKIE_NAME`` so multiple WebUI instances + sharing a hostname (different ports) can use distinct profile-cookie names + instead of trampling each other; browsers scope cookies by host, not + host+port (RFC 6265). The original ``WEBUI_PROFILE_COOKIE_NAME`` is still + honoured as a deprecated fallback (warned once per process, since this is + called on every request). + """ + name = os.getenv(_PROFILE_COOKIE_ENV, '').strip() + if name: + return name + legacy = os.getenv(_LEGACY_PROFILE_COOKIE_ENV, '').strip() + if legacy: + global _legacy_profile_cookie_warned + if not _legacy_profile_cookie_warned: + logger.warning( + '%s is deprecated; use %s instead.', + _LEGACY_PROFILE_COOKIE_ENV, + _PROFILE_COOKIE_ENV, + ) + _legacy_profile_cookie_warned = True + return legacy + return PROFILE_COOKIE_NAME def get_profile_cookie(handler) -> str | None: diff --git a/tests/test_issue803.py b/tests/test_issue803.py index 34a8a544..86835480 100644 --- a/tests/test_issue803.py +++ b/tests/test_issue803.py @@ -13,6 +13,7 @@ Covers: 4. switch_profile(process_wide=False) does NOT mutate process globals 5. Concurrent requests on different threads see independent profiles """ +import logging import os import threading from pathlib import Path @@ -230,6 +231,60 @@ class TestProfileCookieHelpers: assert get_profile_cookie(handler) is None +# ── 1b. Profile cookie name resolution (env > legacy env > default) ─────────── + +class TestProfileCookieNameResolution: + + def test_default_when_unset(self, monkeypatch): + from api.helpers import PROFILE_COOKIE_NAME, get_profile_cookie_name + monkeypatch.delenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', raising=False) + monkeypatch.delenv('WEBUI_PROFILE_COOKIE_NAME', raising=False) + assert get_profile_cookie_name() == PROFILE_COOKIE_NAME + + def test_canonical_env_overrides_default(self, monkeypatch): + from api.helpers import get_profile_cookie_name + monkeypatch.delenv('WEBUI_PROFILE_COOKIE_NAME', raising=False) + monkeypatch.setenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_alt') + assert get_profile_cookie_name() == 'hermes_profile_alt' + + def test_legacy_env_still_honoured(self, monkeypatch): + from api.helpers import get_profile_cookie_name + monkeypatch.delenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', raising=False) + monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_legacy') + assert get_profile_cookie_name() == 'hermes_profile_legacy' + + def test_canonical_takes_precedence_over_legacy(self, monkeypatch): + from api.helpers import get_profile_cookie_name + monkeypatch.setenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', 'canonical') + monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'legacy') + assert get_profile_cookie_name() == 'canonical' + + def test_blank_canonical_falls_back_to_legacy(self, monkeypatch): + from api.helpers import get_profile_cookie_name + monkeypatch.setenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', ' ') + monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_legacy') + assert get_profile_cookie_name() == 'hermes_profile_legacy' + + def test_blank_envs_fall_back_to_default(self, monkeypatch): + from api.helpers import PROFILE_COOKIE_NAME, get_profile_cookie_name + monkeypatch.setenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', ' ') + monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', '') + assert get_profile_cookie_name() == PROFILE_COOKIE_NAME + + def test_legacy_deprecation_warns_only_once(self, monkeypatch, caplog): + # get_profile_cookie_name() runs on every request, so the deprecation + # warning for the legacy env var must be emitted once per process. + import api.helpers as helpers + monkeypatch.delenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', raising=False) + monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_legacy') + monkeypatch.setattr(helpers, '_legacy_profile_cookie_warned', False) + with caplog.at_level(logging.WARNING, logger='api.helpers'): + for _ in range(3): + assert helpers.get_profile_cookie_name() == 'hermes_profile_legacy' + warned = [r for r in caplog.records if 'deprecated' in r.getMessage()] + assert len(warned) == 1 + + # ── 2. Thread-local request context ────────────────────────────────────────── class TestThreadLocalProfileContext: