mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-15 12:20:21 +01:00
refactor: catch payment and invoice error at faspi exceptionhandler level (#2484)
refactor exceptionhandlers into `exception.py` also now always throw payment error when pay_invoice and invoice errors when create_invoice. return a status flag with the detailed error message. with a 520 response
This commit is contained in:
parent
1e752dc3d2
commit
78fc28558c
3 changed files with 139 additions and 140 deletions
|
@ -4,22 +4,17 @@ import importlib
|
|||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from contextlib import asynccontextmanager
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from lnbits.core.crud import get_dbversions, get_installed_extensions
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
|
@ -27,6 +22,7 @@ from lnbits.core.tasks import ( # watchdog_task
|
|||
killswitch_task,
|
||||
wait_for_paid_invoices,
|
||||
)
|
||||
from lnbits.exceptions import register_exception_handlers
|
||||
from lnbits.settings import settings
|
||||
from lnbits.tasks import (
|
||||
cancel_all_tasks,
|
||||
|
@ -53,7 +49,6 @@ from .extension_manager import (
|
|||
get_valid_extensions,
|
||||
version_parse,
|
||||
)
|
||||
from .helpers import template_renderer
|
||||
from .middleware import (
|
||||
CustomGZipMiddleware,
|
||||
ExtensionsRedirectMiddleware,
|
||||
|
@ -429,82 +424,3 @@ def register_async_tasks():
|
|||
if settings.lnbits_admin_ui:
|
||||
server_log_task = initialize_server_websocket_logger()
|
||||
create_permanent_task(server_log_task)
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI):
|
||||
@app.exception_handler(Exception)
|
||||
async def exception_handler(request: Request, exc: Exception):
|
||||
etype, _, tb = sys.exc_info()
|
||||
traceback.print_exception(etype, exc, tb)
|
||||
logger.error(f"Exception: {exc!s}")
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
if (
|
||||
request.headers
|
||||
and "accept" in request.headers
|
||||
and "text/html" in request.headers["accept"]
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
request, "error.html", {"err": f"Error: {exc!s}"}
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
):
|
||||
logger.error(f"RequestValidationError: {exc!s}")
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
|
||||
if (
|
||||
request.headers
|
||||
and "accept" in request.headers
|
||||
and "text/html" in request.headers["accept"]
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"error.html",
|
||||
{"err": f"Error: {exc!s}"},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
logger.error(f"HTTPException {exc.status_code}: {exc.detail}")
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
|
||||
if (
|
||||
request.headers
|
||||
and "accept" in request.headers
|
||||
and "text/html" in request.headers["accept"]
|
||||
):
|
||||
if exc.headers and "token-expired" in exc.headers:
|
||||
response = RedirectResponse("/")
|
||||
response.delete_cookie("cookie_access_token")
|
||||
response.delete_cookie("is_lnbits_user_authorized")
|
||||
response.set_cookie("is_access_token_expired", "true")
|
||||
return response
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"error.html",
|
||||
{
|
||||
"request": request,
|
||||
"err": f"HTTP Error {exc.status_code}: {exc.detail}",
|
||||
},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.detail},
|
||||
)
|
||||
|
|
|
@ -55,8 +55,6 @@ from ..crud import (
|
|||
update_pending_payments,
|
||||
)
|
||||
from ..services import (
|
||||
InvoiceError,
|
||||
PaymentError,
|
||||
check_transaction_status,
|
||||
create_invoice,
|
||||
fee_reserve_total,
|
||||
|
@ -150,33 +148,25 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
|||
memo = ""
|
||||
|
||||
async with db.connect() as conn:
|
||||
try:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=wallet.id,
|
||||
amount=data.amount,
|
||||
memo=memo,
|
||||
currency=data.unit,
|
||||
description_hash=description_hash,
|
||||
unhashed_description=unhashed_description,
|
||||
expiry=data.expiry,
|
||||
extra=data.extra,
|
||||
webhook=data.webhook,
|
||||
internal=data.internal,
|
||||
conn=conn,
|
||||
)
|
||||
# NOTE: we get the checking_id with a seperate query because create_invoice
|
||||
# does not return it and it would be a big hustle to change its return type
|
||||
# (used across extensions)
|
||||
payment_db = await get_standalone_payment(payment_hash, conn=conn)
|
||||
assert payment_db is not None, "payment not found"
|
||||
checking_id = payment_db.checking_id
|
||||
except InvoiceError as exc:
|
||||
return JSONResponse(
|
||||
status_code=520,
|
||||
content={"detail": exc.message, "status": exc.status},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise exc
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=wallet.id,
|
||||
amount=data.amount,
|
||||
memo=memo,
|
||||
currency=data.unit,
|
||||
description_hash=description_hash,
|
||||
unhashed_description=unhashed_description,
|
||||
expiry=data.expiry,
|
||||
extra=data.extra,
|
||||
webhook=data.webhook,
|
||||
internal=data.internal,
|
||||
conn=conn,
|
||||
)
|
||||
# NOTE: we get the checking_id with a seperate query because create_invoice
|
||||
# does not return it and it would be a big hustle to change its return type
|
||||
# (used across extensions)
|
||||
payment_db = await get_standalone_payment(payment_hash, conn=conn)
|
||||
assert payment_db is not None, "payment not found"
|
||||
checking_id = payment_db.checking_id
|
||||
|
||||
invoice = bolt11.decode(payment_request)
|
||||
|
||||
|
@ -213,28 +203,6 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
|||
}
|
||||
|
||||
|
||||
async def api_payments_pay_invoice(
|
||||
bolt11: str, wallet: Wallet, extra: Optional[dict] = None
|
||||
):
|
||||
try:
|
||||
payment_hash = await pay_invoice(
|
||||
wallet_id=wallet.id, payment_request=bolt11, extra=extra
|
||||
)
|
||||
except PaymentError as exc:
|
||||
return JSONResponse(
|
||||
status_code=520,
|
||||
content={"detail": exc.message, "status": exc.status},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise exc
|
||||
|
||||
return {
|
||||
"payment_hash": payment_hash,
|
||||
# maintain backwards compatibility with API clients:
|
||||
"checking_id": payment_hash,
|
||||
}
|
||||
|
||||
|
||||
@payment_router.post(
|
||||
"",
|
||||
summary="Create or pay an invoice",
|
||||
|
@ -247,6 +215,11 @@ async def api_payments_pay_invoice(
|
|||
field to supply the BOLT11 invoice to be paid.
|
||||
""",
|
||||
status_code=HTTPStatus.CREATED,
|
||||
responses={
|
||||
400: {"description": "Invalid BOLT11 string or missing fields."},
|
||||
401: {"description": "Invoice (or Admin) key required."},
|
||||
520: {"description": "Payment or Invoice error."},
|
||||
},
|
||||
)
|
||||
async def api_payments_create(
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
|
@ -258,9 +231,18 @@ async def api_payments_create(
|
|||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="BOLT11 string is invalid or not given",
|
||||
)
|
||||
return await api_payments_pay_invoice(
|
||||
invoice_data.bolt11, wallet.wallet, invoice_data.extra
|
||||
) # admin key
|
||||
|
||||
payment_hash = await pay_invoice(
|
||||
wallet_id=wallet.wallet.id,
|
||||
payment_request=invoice_data.bolt11,
|
||||
extra=invoice_data.extra,
|
||||
)
|
||||
return {
|
||||
"payment_hash": payment_hash,
|
||||
# maintain backwards compatibility with API clients:
|
||||
"checking_id": payment_hash,
|
||||
}
|
||||
|
||||
elif not invoice_data.out:
|
||||
# invoice key
|
||||
return await api_payments_create_invoice(invoice_data, wallet.wallet)
|
||||
|
|
101
lnbits/exceptions.py
Normal file
101
lnbits/exceptions.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
import sys
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse, RedirectResponse, Response
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.services import InvoiceError, PaymentError
|
||||
|
||||
from .helpers import template_renderer
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI):
|
||||
register_exception_handler(app)
|
||||
register_request_validation_exception_handler(app)
|
||||
register_http_exception_handler(app)
|
||||
register_payment_error_handler(app)
|
||||
register_invoice_error_handler(app)
|
||||
|
||||
|
||||
def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
if (
|
||||
request.headers
|
||||
and "accept" in request.headers
|
||||
and "text/html" in request.headers["accept"]
|
||||
):
|
||||
if (
|
||||
isinstance(exc, HTTPException)
|
||||
and exc.headers
|
||||
and "token-expired" in exc.headers
|
||||
):
|
||||
response = RedirectResponse("/")
|
||||
response.delete_cookie("cookie_access_token")
|
||||
response.delete_cookie("is_lnbits_user_authorized")
|
||||
response.set_cookie("is_access_token_expired", "true")
|
||||
return response
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request, "error.html", {"err": f"Error: {exc!s}"}
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def register_exception_handler(app: FastAPI):
|
||||
@app.exception_handler(Exception)
|
||||
async def exception_handler(request: Request, exc: Exception):
|
||||
etype, _, tb = sys.exc_info()
|
||||
traceback.print_exception(etype, exc, tb)
|
||||
logger.error(f"Exception: {exc!s}")
|
||||
return render_html_error(request, exc) or JSONResponse(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
def register_request_validation_exception_handler(app: FastAPI):
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
):
|
||||
logger.error(f"RequestValidationError: {exc!s}")
|
||||
return render_html_error(request, exc) or JSONResponse(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
content={"detail": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
def register_http_exception_handler(app: FastAPI):
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
logger.error(f"HTTPException {exc.status_code}: {exc.detail}")
|
||||
return render_html_error(request, exc) or JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.detail},
|
||||
)
|
||||
|
||||
|
||||
def register_payment_error_handler(app: FastAPI):
|
||||
@app.exception_handler(PaymentError)
|
||||
async def payment_error_handler(request: Request, exc: PaymentError):
|
||||
logger.error(f"PaymentError: {exc.message}, {exc.status}")
|
||||
return JSONResponse(
|
||||
status_code=520,
|
||||
content={"detail": exc.message, "status": exc.status},
|
||||
)
|
||||
|
||||
|
||||
def register_invoice_error_handler(app: FastAPI):
|
||||
@app.exception_handler(InvoiceError)
|
||||
async def invoice_error_handler(request: Request, exc: InvoiceError):
|
||||
logger.error(f"InvoiceError: {exc.message}, Status: {exc.status}")
|
||||
return JSONResponse(
|
||||
status_code=520,
|
||||
content={"detail": exc.message, "status": exc.status},
|
||||
)
|
Loading…
Add table
Reference in a new issue