mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-08 22:22:17 +02:00
fix(capabilities): map any executor fault to 502 at the REST door
This commit is contained in:
parent
0844dd581f
commit
b5d221c19c
2 changed files with 82 additions and 1 deletions
|
|
@ -13,6 +13,7 @@ from app.capabilities.core.billing import charge_capability, gate_capability
|
||||||
from app.capabilities.core.store import all_capabilities
|
from app.capabilities.core.store import all_capabilities
|
||||||
from app.capabilities.core.types import Capability, CapabilityContext
|
from app.capabilities.core.types import Capability, CapabilityContext
|
||||||
from app.db import get_async_session
|
from app.db import get_async_session
|
||||||
|
from app.exceptions import ExternalServiceError, SurfSenseError
|
||||||
from app.services.web_crawl_credit_service import InsufficientCreditsError
|
from app.services.web_crawl_credit_service import InsufficientCreditsError
|
||||||
from app.users import get_auth_context
|
from app.users import get_auth_context
|
||||||
from app.utils.rbac import check_workspace_access
|
from app.utils.rbac import check_workspace_access
|
||||||
|
|
@ -55,7 +56,17 @@ def _register_verb(router: APIRouter, capability: Capability) -> None:
|
||||||
"required_micros": exc.required_micros,
|
"required_micros": exc.required_micros,
|
||||||
},
|
},
|
||||||
) from exc
|
) from exc
|
||||||
output = await executor(payload)
|
|
||||||
|
try:
|
||||||
|
output = await executor(payload)
|
||||||
|
except (SurfSenseError, HTTPException):
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise ExternalServiceError(
|
||||||
|
f"The '{capability.name}' capability failed due to an upstream error.",
|
||||||
|
code="CAPABILITY_UPSTREAM_ERROR",
|
||||||
|
) from exc
|
||||||
|
|
||||||
await charge_capability(output, unit, ctx)
|
await charge_capability(output, unit, ctx)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,76 @@ async def test_rate_limit_blocks_the_workspace(monkeypatch):
|
||||||
assert resp.status_code == 429
|
assert resp.status_code == 429
|
||||||
|
|
||||||
|
|
||||||
|
def _register_surfsense_handler(app: FastAPI) -> None:
|
||||||
|
"""Minimal stand-in for the app's global SurfSenseError handler."""
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
from app.exceptions import SurfSenseError
|
||||||
|
|
||||||
|
async def _handler(_request, exc: SurfSenseError):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={"code": exc.code, "message": exc.message},
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_exception_handler(SurfSenseError, _handler)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_executor_fault_becomes_502(monkeypatch):
|
||||||
|
"""Any non-SurfSense executor error is surfaced as a clean 502, not a 500."""
|
||||||
|
|
||||||
|
async def _boom(_payload: _EchoInput) -> _EchoOutput:
|
||||||
|
raise RuntimeError("upstream provider exploded")
|
||||||
|
|
||||||
|
boom = Capability(
|
||||||
|
name="test.boom",
|
||||||
|
description="Always fails for tests.",
|
||||||
|
input_schema=_EchoInput,
|
||||||
|
output_schema=_EchoOutput,
|
||||||
|
executor=_boom,
|
||||||
|
billing_unit=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
app = _build_app([boom], monkeypatch)
|
||||||
|
_register_surfsense_handler(app)
|
||||||
|
async with _client(app) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/v1/workspaces/7/capabilities/test.boom",
|
||||||
|
json={"value": "hi"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 502
|
||||||
|
assert resp.json()["code"] == "CAPABILITY_UPSTREAM_ERROR"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_surfsense_error_passes_through(monkeypatch):
|
||||||
|
"""Intentional, status-carrying errors (e.g. a 403 wall) are not remapped."""
|
||||||
|
from app.exceptions import ForbiddenError
|
||||||
|
|
||||||
|
async def _forbidden(_payload: _EchoInput) -> _EchoOutput:
|
||||||
|
raise ForbiddenError("sign in required", code="GOOGLE_SIGNIN_REQUIRED")
|
||||||
|
|
||||||
|
forbidden = Capability(
|
||||||
|
name="test.forbidden",
|
||||||
|
description="Raises a domain 403 for tests.",
|
||||||
|
input_schema=_EchoInput,
|
||||||
|
output_schema=_EchoOutput,
|
||||||
|
executor=_forbidden,
|
||||||
|
billing_unit=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
app = _build_app([forbidden], monkeypatch)
|
||||||
|
_register_surfsense_handler(app)
|
||||||
|
async with _client(app) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/v1/workspaces/7/capabilities/test.forbidden",
|
||||||
|
json={"value": "hi"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 403
|
||||||
|
assert resp.json()["code"] == "GOOGLE_SIGNIN_REQUIRED"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_success_charges_once(monkeypatch):
|
async def test_success_charges_once(monkeypatch):
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue