瀏覽代碼

fix(oidc): survive a failing rollback in the never-raise handler too

The recovery rollback after a failed commit was itself unguarded, so a
rollback that raises on a wedged connection would still take the boot
down -- the exact failure the never-raise contract exists to prevent.
Suppressed; the caller's `async with` discards the session regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q8EAf9Rj7ZHL92sPnXYxy
Marian 1 月之前
父節點
當前提交
6eea61dc78
共有 2 個文件被更改,包括 30 次插入2 次删除
  1. 7 2
      backend/app/core/oidc_env.py
  2. 23 0
      backend/tests/integration/test_oidc_env_apply.py

+ 7 - 2
backend/app/core/oidc_env.py

@@ -9,6 +9,7 @@ check the UI enforces.
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
+import contextlib
 import logging
 import logging
 import os
 import os
 
 
@@ -100,8 +101,12 @@ async def apply_env_oidc_provider(db: AsyncSession) -> None:
         # Never str(exc): a DB error message can echo a configured value. Class only.
         # Never str(exc): a DB error message can echo a configured value. Class only.
         logger.error("BAMBUDDY_OIDC_* could not be applied: %s", type(exc).__name__)
         logger.error("BAMBUDDY_OIDC_* could not be applied: %s", type(exc).__name__)
         # A commit may have half-applied; roll back so the shared session is
         # A commit may have half-applied; roll back so the shared session is
-        # left clean for the rest of startup.
-        await db.rollback()
+        # left clean for the rest of startup. Suppressed because rollback on a
+        # wedged connection can itself raise -- and the whole point here is that
+        # nothing in this path takes the boot down. The session is discarded by
+        # the caller's `async with` regardless.
+        with contextlib.suppress(Exception):
+            await db.rollback()
 
 
 
 
 async def _apply_env_oidc_provider(db: AsyncSession) -> None:
 async def _apply_env_oidc_provider(db: AsyncSession) -> None:

+ 23 - 0
backend/tests/integration/test_oidc_env_apply.py

@@ -220,6 +220,29 @@ async def test_a_commit_failure_is_survivable_and_leaks_nothing(db_session, monk
     assert "leaked-secret" not in caplog.text  # ...but nothing from the message
     assert "leaked-secret" not in caplog.text  # ...but nothing from the message
 
 
 
 
+@pytest.mark.asyncio
+async def test_a_failing_rollback_is_also_survivable(db_session, monkeypatch, caplog):
+    """The handler rolls back after a failed commit -- but rollback on a wedged
+    connection can raise too, and 'never raises' has to hold for that as well
+    or the boot dies on the recovery path. The rollback is suppressed."""
+
+    async def _raise_on_commit():
+        raise RuntimeError("database is locked")
+
+    async def _raise_on_rollback():
+        raise RuntimeError("connection is closed")
+
+    monkeypatch.setattr(db_session, "commit", _raise_on_commit)
+    monkeypatch.setattr(db_session, "rollback", _raise_on_rollback)
+    _configure(monkeypatch, BAMBUDDY_OIDC_CLIENT_SECRET="leaked-secret")
+
+    with caplog.at_level(logging.ERROR):
+        await apply_env_oidc_provider(db_session)  # must not raise, even here
+
+    assert "could not be applied" in caplog.text
+    assert "leaked-secret" not in caplog.text
+
+
 @pytest.mark.asyncio
 @pytest.mark.asyncio
 async def test_applying_twice_without_changes_is_a_no_op(db_session, monkeypatch):
 async def test_applying_twice_without_changes_is_a_no_op(db_session, monkeypatch):
     """Every boot re-applies; the second run must not create a second row."""
     """Every boot re-applies; the second run must not create a second row."""