diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cf9aa1b..7f57289 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,9 @@ All notable changes to Palinode. Format follows [Keep a Changelog](https://keepa ### Fixed +- Preserve the embedding-backend 503 response for `/triggers` and + `/check-triggers` instead of converting outages into generic 500 errors. + ### Removed ### Security diff --git a/palinode/api/_util.py b/palinode/api/_util.py index 71251bc..67787d7 100644 --- a/palinode/api/_util.py +++ b/palinode/api/_util.py @@ -19,6 +19,7 @@ from fastapi import HTTPException +from palinode.core import embedder from palinode.core.config import config from palinode.core.retrieval_log import RetrievalLogger @@ -43,6 +44,18 @@ def _safe_500(e: Exception, context: str = "Internal error") -> HTTPException: return HTTPException(status_code=500, detail=context) +def _embedding_unavailable_503( + error: embedder.EmbeddingUnavailable, + operation: str, +) -> HTTPException: + """Preserve the typed outage message without logging a traceback.""" + logger.warning( + "%s unavailable outcome=embedding_unavailable", + operation, + ) + return HTTPException(status_code=503, detail=str(error)) + + def _project_from_cwd(cwd: str | None) -> str | None: """Derive a project slug from a CWD path's basename. diff --git a/palinode/api/routers/search.py b/palinode/api/routers/search.py index 2aded25..fd72b26 100644 --- a/palinode/api/routers/search.py +++ b/palinode/api/routers/search.py @@ -9,7 +9,11 @@ from palinode.core.config import config from palinode.core.parity import CATEGORIES, MEMORY_TYPES from palinode.core.path_guard import to_rel_path -from palinode.api._util import _retrieval_logger, _safe_500 +from palinode.api._util import ( + _embedding_unavailable_503, + _retrieval_logger, + _safe_500, +) from palinode.api.rate_limit import _RATE_LIMIT_SEARCH, _check_rate_limit from palinode.api.search_helpers import ( _compute_effective_date_after, @@ -32,18 +36,6 @@ _VISIBILITY_OVERFETCH = 5 -def _embedding_unavailable_503( - error: embedder.EmbeddingUnavailable, - operation: str, -) -> HTTPException: - """Preserve the typed outage message without logging a traceback.""" - logger.warning( - "%s unavailable op=search outcome=embedding_unavailable", - operation, - ) - return HTTPException(status_code=503, detail=str(error)) - - def _resolve_scope_chain( context: list[str] | None = None, session_id: str | None = None, diff --git a/palinode/api/routers/triggers.py b/palinode/api/routers/triggers.py index 84d1253..f939f1a 100644 --- a/palinode/api/routers/triggers.py +++ b/palinode/api/routers/triggers.py @@ -5,7 +5,7 @@ from fastapi import APIRouter from pydantic import BaseModel -from palinode.api._util import _safe_500 +from palinode.api._util import _embedding_unavailable_503, _safe_500 from palinode.core import embedder, store router = APIRouter() @@ -43,6 +43,8 @@ def create_trigger_api(req: TriggerRequest) -> dict[str, Any]: cooldown_hours=req.cooldown_hours or 24 ) return {"id": trigger_id, "status": "created"} + except embedder.EmbeddingUnavailable as e: + raise _embedding_unavailable_503(e, "Trigger creation") from None except Exception as e: raise _safe_500(e, "Trigger creation failed") @@ -72,5 +74,7 @@ def check_triggers_api(req: CheckTriggersRequest) -> list[dict[str, Any]]: cooldown_bypass=req.cooldown_bypass or False ) return results + except embedder.EmbeddingUnavailable as e: + raise _embedding_unavailable_503(e, "Trigger check") from None except Exception as e: raise _safe_500(e, "Trigger check failed") diff --git a/tests/test_search_embedder_outage.py b/tests/test_search_embedder_outage.py index 64b7571..18eb69b 100644 --- a/tests/test_search_embedder_outage.py +++ b/tests/test_search_embedder_outage.py @@ -110,6 +110,33 @@ def test_other_embedding_search_operations_preserve_typed_503( assert "palinode doctor" in res.json()["detail"] +def test_create_trigger_preserves_typed_embedding_503(client): + with patch("palinode.core.embedder.embed", side_effect=_boom): + res = client.post( + "/triggers", + json={ + "description": "deploy the memory service", + "memory_file": "projects/memory.md", + }, + ) + + assert res.status_code == 503 + assert "Embedding backend unavailable" in res.json()["detail"] + assert "palinode doctor" in res.json()["detail"] + + +def test_check_triggers_preserves_typed_embedding_503(client): + with patch("palinode.core.embedder.embed", side_effect=_boom): + res = client.post( + "/check-triggers", + json={"query": "what should I remember about deployment?"}, + ) + + assert res.status_code == 503 + assert "Embedding backend unavailable" in res.json()["detail"] + assert "palinode doctor" in res.json()["detail"] + + def test_save_keeps_200_and_index_error_with_same_dead_embedder(client, dead_embedder): res = client.post( "/save",