mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-30 15:26:14 +00:00
fix: tighten redemption error mapping and stop X-Cashu raw-text leak
- Anchor the broad invalid/decode buckets to co-occur with "token" so internal faults (e.g. "Invalid isoformat string") fall through to 500 instead of masquerading as a 401 token error. - Map the "estimate fees"/"exceed token amount" wallet errors to the specific "too small to cover swap fees" message instead of the generic bucket. - Stop the X-Cashu redemption path from interpolating raw mint/exception text into mint_error/cashu_error responses (both duplicated blocks). - Add tests for the fee-gap mapping and the anchored gate.
This commit is contained in:
+11
-2
@@ -91,11 +91,20 @@ def redemption_error_to_http_exception(error: Exception) -> HTTPException:
|
||||
status_code = 400
|
||||
if "already spent" in lowered:
|
||||
message = "Cashu token already spent"
|
||||
elif "insufficient" in lowered or "melt fee" in lowered:
|
||||
elif (
|
||||
"insufficient" in lowered
|
||||
or "melt fee" in lowered
|
||||
or "exceed token amount" in lowered
|
||||
or "estimate fees" in lowered
|
||||
):
|
||||
message = "Token value is too small to cover swap fees"
|
||||
elif "failed to melt" in lowered:
|
||||
message = "Failed to swap token from foreign mint"
|
||||
elif "invalid" in lowered or "decode" in lowered:
|
||||
elif ("invalid" in lowered or "decode" in lowered) and "token" in lowered:
|
||||
# Anchor the broad buckets to "token" so internal faults whose text
|
||||
# merely contains "invalid"/"decode" (e.g. "invalid literal",
|
||||
# SQLAlchemy "Invalid …") fall through to the generic 500 below
|
||||
# instead of masquerading as a 401 token error.
|
||||
message = "Invalid Cashu token"
|
||||
status_code = 401
|
||||
elif isinstance(error, ValueError):
|
||||
|
||||
@@ -4049,7 +4049,7 @@ class BaseUpstreamProvider:
|
||||
if "mint error" in error_message.lower():
|
||||
return create_error_response(
|
||||
"mint_error",
|
||||
f"CASHU mint error: {error_message}",
|
||||
"CASHU mint error while processing token",
|
||||
422,
|
||||
request=request,
|
||||
token=x_cashu_token,
|
||||
@@ -4057,7 +4057,7 @@ class BaseUpstreamProvider:
|
||||
|
||||
return create_error_response(
|
||||
"cashu_error",
|
||||
f"CASHU token processing failed: {error_message}",
|
||||
"CASHU token processing failed",
|
||||
400,
|
||||
request=request,
|
||||
token=x_cashu_token,
|
||||
@@ -4717,7 +4717,7 @@ class BaseUpstreamProvider:
|
||||
if "mint error" in error_message.lower():
|
||||
return create_error_response(
|
||||
"mint_error",
|
||||
f"CASHU mint error: {error_message}",
|
||||
"CASHU mint error while processing token",
|
||||
422,
|
||||
request=request,
|
||||
token=x_cashu_token,
|
||||
@@ -4725,7 +4725,7 @@ class BaseUpstreamProvider:
|
||||
|
||||
return create_error_response(
|
||||
"cashu_error",
|
||||
f"CASHU token processing failed: {error_message}",
|
||||
"CASHU token processing failed",
|
||||
400,
|
||||
request=request,
|
||||
token=x_cashu_token,
|
||||
|
||||
@@ -75,6 +75,13 @@ async def test_failed_first_cashu_redemption_rolls_back_empty_api_key(
|
||||
400,
|
||||
"Token value is too small to cover swap fees",
|
||||
),
|
||||
(
|
||||
ValueError(
|
||||
"Failed to estimate fees: Fees (7 sat) exceed token amount (5 sat)"
|
||||
),
|
||||
400,
|
||||
"Token value is too small to cover swap fees",
|
||||
),
|
||||
(
|
||||
ValueError(
|
||||
"Failed to melt token from foreign mint http://foreign:3338: boom"
|
||||
@@ -158,3 +165,37 @@ async def test_unexpected_redemption_error_returns_internal_error(
|
||||
assert error_detail["code"] == "internal_error"
|
||||
assert "/var/lib/secret" not in error_detail["message"]
|
||||
assert await session.get(ApiKey, hashed_key) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_internal_error_with_invalid_keyword_does_not_masquerade(
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""A non-wallet fault whose text merely contains "invalid" (but not
|
||||
"token") must fall through to a generic 500, not a 401 token error.
|
||||
|
||||
Guards the anchored `"invalid"/"decode"` + `"token"` gate against stdlib/
|
||||
driver strings like "Invalid isoformat string" leaking as token errors."""
|
||||
token = "cashuAinternal_fault_mentions_invalid"
|
||||
hashed_key = hashlib.sha256(token.encode()).hexdigest()
|
||||
token_obj = SimpleNamespace(mint="http://mint:3338", unit="sat")
|
||||
|
||||
from routstr.core.settings import settings
|
||||
|
||||
with (
|
||||
patch.object(settings, "cashu_mints", ["http://mint:3338"]),
|
||||
patch("routstr.auth.deserialize_token_from_string", return_value=token_obj),
|
||||
patch(
|
||||
"routstr.auth.credit_balance",
|
||||
new=AsyncMock(
|
||||
side_effect=RuntimeError("Invalid isoformat string: '2020-13-99'")
|
||||
),
|
||||
),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await validate_bearer_key(token, session)
|
||||
|
||||
assert exc_info.value.status_code == 500
|
||||
detail = cast(dict[str, dict[str, str]], exc_info.value.detail)
|
||||
assert detail["error"]["code"] == "internal_error"
|
||||
assert await session.get(ApiKey, hashed_key) is None
|
||||
|
||||
Reference in New Issue
Block a user