mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
clean up
This commit is contained in:
+2
-2
@@ -36,8 +36,8 @@ HTTP response, `error` is available at the top level and mirrored under
|
|||||||
| Unexpected invoice creation failure | 500 | `api_error` | `invoice_creation_failed` |
|
| Unexpected invoice creation failure | 500 | `api_error` | `invoice_creation_failed` |
|
||||||
|
|
||||||
Request validation failures, including non-positive or excessive amounts, use
|
Request validation failures, including non-positive or excessive amounts, use
|
||||||
FastAPI's standard 422 validation response. Only the 503 mint failures are retryable. Use backoff and honor any mint
|
FastAPI's standard 422 validation response. Only the 503 mint failures are retryable. Use backoff and honor the
|
||||||
cooldown. The compatibility endpoints `/lightning/*` and
|
`Retry-After` header when present. The compatibility endpoints `/lightning/*` and
|
||||||
`/v1/balance/lightning/*` retain their original string `detail` errors and
|
`/v1/balance/lightning/*` retain their original string `detail` errors and
|
||||||
legacy status behavior.
|
legacy status behavior.
|
||||||
|
|
||||||
|
|||||||
+18
-5
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
@@ -244,10 +245,8 @@ async def _request_mint_with_fallback(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if all_rate_limited:
|
if all_rate_limited:
|
||||||
retry_after = max(
|
slowest = max(candidates, key=mint_cooldown_remaining)
|
||||||
(mint_cooldown_remaining(mint) for mint in candidates), default=0.0
|
raise MintCooldownError(slowest, mint_cooldown_remaining(slowest))
|
||||||
)
|
|
||||||
raise MintCooldownError("all configured mints", retry_after)
|
|
||||||
raise MintConnectionError(f"All mints failed for request_mint: {tried}")
|
raise MintConnectionError(f"All mints failed for request_mint: {tried}")
|
||||||
|
|
||||||
|
|
||||||
@@ -279,6 +278,7 @@ def _invoice_error(
|
|||||||
*,
|
*,
|
||||||
structured: bool,
|
structured: bool,
|
||||||
legacy_message: str | None = None,
|
legacy_message: str | None = None,
|
||||||
|
headers: dict[str, str] | None = None,
|
||||||
) -> HTTPException:
|
) -> HTTPException:
|
||||||
"""Build either the legacy string detail or the v2 typed envelope."""
|
"""Build either the legacy string detail or the v2 typed envelope."""
|
||||||
detail: str | dict[str, dict[str, str]]
|
detail: str | dict[str, dict[str, str]]
|
||||||
@@ -286,7 +286,18 @@ def _invoice_error(
|
|||||||
detail = {"error": {"message": message, "type": error_type, "code": code}}
|
detail = {"error": {"message": message, "type": error_type, "code": code}}
|
||||||
else:
|
else:
|
||||||
detail = legacy_message or message
|
detail = legacy_message or message
|
||||||
return HTTPException(status_code=status_code, detail=detail)
|
return HTTPException(status_code=status_code, detail=detail, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
def _mint_retry_after(error: BaseException) -> int | None:
|
||||||
|
current: BaseException | None = error
|
||||||
|
seen: set[int] = set()
|
||||||
|
while current is not None and id(current) not in seen:
|
||||||
|
seen.add(id(current))
|
||||||
|
if isinstance(current, MintCooldownError):
|
||||||
|
return math.ceil(current.retry_after_seconds)
|
||||||
|
current = current.__cause__ or current.__context__
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _invoice_creation_error(error: Exception, *, structured: bool) -> HTTPException:
|
def _invoice_creation_error(error: Exception, *, structured: bool) -> HTTPException:
|
||||||
@@ -296,12 +307,14 @@ def _invoice_creation_error(error: Exception, *, structured: bool) -> HTTPExcept
|
|||||||
status_code=500, detail="Failed to create Lightning invoice"
|
status_code=500, detail="Failed to create Lightning invoice"
|
||||||
)
|
)
|
||||||
if is_mint_rate_limited(error):
|
if is_mint_rate_limited(error):
|
||||||
|
retry_after = _mint_retry_after(error)
|
||||||
return _invoice_error(
|
return _invoice_error(
|
||||||
503,
|
503,
|
||||||
"Cashu mint rate-limited; retry after cooldown",
|
"Cashu mint rate-limited; retry after cooldown",
|
||||||
"mint_rate_limited",
|
"mint_rate_limited",
|
||||||
"lightning_mint_rate_limited",
|
"lightning_mint_rate_limited",
|
||||||
structured=True,
|
structured=True,
|
||||||
|
headers={"Retry-After": str(retry_after)} if retry_after else None,
|
||||||
)
|
)
|
||||||
if is_mint_connection_error(error):
|
if is_mint_connection_error(error):
|
||||||
return _invoice_error(
|
return _invoice_error(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from httpx import AsyncClient
|
|||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from routstr.core.db import ApiKey
|
from routstr.core.db import ApiKey
|
||||||
|
from routstr.mint import MintCooldownError
|
||||||
from routstr.wallet import MintConnectionError
|
from routstr.wallet import MintConnectionError
|
||||||
|
|
||||||
RIP08_PATH = "/lightning/invoice"
|
RIP08_PATH = "/lightning/invoice"
|
||||||
@@ -317,6 +318,22 @@ async def test_create_invoice_maps_mint_failures(
|
|||||||
assert resp.json()["detail"]["error"]["code"] == code
|
assert resp.json()["detail"]["error"]["code"] == code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_v2_create_invoice_rate_limited_sets_retry_after(
|
||||||
|
integration_client: AsyncClient,
|
||||||
|
) -> None:
|
||||||
|
with patch(
|
||||||
|
"routstr.lightning.generate_lightning_invoice",
|
||||||
|
side_effect=MintCooldownError("https://mint.example.com", 12.4),
|
||||||
|
):
|
||||||
|
resp = await integration_client.post(V2_PATH, json={"amount_sats": 100})
|
||||||
|
|
||||||
|
assert resp.status_code == 503
|
||||||
|
assert resp.headers["Retry-After"] == "13"
|
||||||
|
assert resp.json()["detail"]["error"]["code"] == "lightning_mint_rate_limited"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_compatibility_create_failure_keeps_generic_error(
|
async def test_compatibility_create_failure_keeps_generic_error(
|
||||||
|
|||||||
Reference in New Issue
Block a user