mirror of
https://github.com/lnbits/lnbits.git
synced 2025-12-18 08:31:33 +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:
@@ -4,22 +4,17 @@ import importlib
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from http import HTTPStatus
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Request
|
from fastapi import FastAPI
|
||||||
from fastapi.exceptions import RequestValidationError
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from slowapi import Limiter
|
from slowapi import Limiter
|
||||||
from slowapi.util import get_remote_address
|
from slowapi.util import get_remote_address
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_dbversions, get_installed_extensions
|
from lnbits.core.crud import get_dbversions, get_installed_extensions
|
||||||
from lnbits.core.helpers import migrate_extension_database
|
from lnbits.core.helpers import migrate_extension_database
|
||||||
@@ -27,6 +22,7 @@ from lnbits.core.tasks import ( # watchdog_task
|
|||||||
killswitch_task,
|
killswitch_task,
|
||||||
wait_for_paid_invoices,
|
wait_for_paid_invoices,
|
||||||
)
|
)
|
||||||
|
from lnbits.exceptions import register_exception_handlers
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.tasks import (
|
from lnbits.tasks import (
|
||||||
cancel_all_tasks,
|
cancel_all_tasks,
|
||||||
@@ -53,7 +49,6 @@ from .extension_manager import (
|
|||||||
get_valid_extensions,
|
get_valid_extensions,
|
||||||
version_parse,
|
version_parse,
|
||||||
)
|
)
|
||||||
from .helpers import template_renderer
|
|
||||||
from .middleware import (
|
from .middleware import (
|
||||||
CustomGZipMiddleware,
|
CustomGZipMiddleware,
|
||||||
ExtensionsRedirectMiddleware,
|
ExtensionsRedirectMiddleware,
|
||||||
@@ -429,82 +424,3 @@ def register_async_tasks():
|
|||||||
if settings.lnbits_admin_ui:
|
if settings.lnbits_admin_ui:
|
||||||
server_log_task = initialize_server_websocket_logger()
|
server_log_task = initialize_server_websocket_logger()
|
||||||
create_permanent_task(server_log_task)
|
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,
|
update_pending_payments,
|
||||||
)
|
)
|
||||||
from ..services import (
|
from ..services import (
|
||||||
InvoiceError,
|
|
||||||
PaymentError,
|
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
create_invoice,
|
create_invoice,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
@@ -150,7 +148,6 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
|||||||
memo = ""
|
memo = ""
|
||||||
|
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
try:
|
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=wallet.id,
|
wallet_id=wallet.id,
|
||||||
amount=data.amount,
|
amount=data.amount,
|
||||||
@@ -170,13 +167,6 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
|||||||
payment_db = await get_standalone_payment(payment_hash, conn=conn)
|
payment_db = await get_standalone_payment(payment_hash, conn=conn)
|
||||||
assert payment_db is not None, "payment not found"
|
assert payment_db is not None, "payment not found"
|
||||||
checking_id = payment_db.checking_id
|
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
|
|
||||||
|
|
||||||
invoice = bolt11.decode(payment_request)
|
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(
|
@payment_router.post(
|
||||||
"",
|
"",
|
||||||
summary="Create or pay an invoice",
|
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.
|
field to supply the BOLT11 invoice to be paid.
|
||||||
""",
|
""",
|
||||||
status_code=HTTPStatus.CREATED,
|
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(
|
async def api_payments_create(
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
@@ -258,9 +231,18 @@ async def api_payments_create(
|
|||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="BOLT11 string is invalid or not given",
|
detail="BOLT11 string is invalid or not given",
|
||||||
)
|
)
|
||||||
return await api_payments_pay_invoice(
|
|
||||||
invoice_data.bolt11, wallet.wallet, invoice_data.extra
|
payment_hash = await pay_invoice(
|
||||||
) # admin key
|
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:
|
elif not invoice_data.out:
|
||||||
# invoice key
|
# invoice key
|
||||||
return await api_payments_create_invoice(invoice_data, wallet.wallet)
|
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},
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user