feat(coordinator-api): add global exception handler and rate limiting to marketplace and exchange endpoints
- Add general exception handler to catch all unhandled exceptions with structured error responses - Add structured logging to validation error handler with request context - Implement slowapi rate limiting on marketplace endpoints (100/min list, 50/min stats, 30/min bid) - Implement slowapi rate limiting on exchange payment creation (20/min) - Add Request parameter to rate-limited endpoints for slow
This commit is contained in:
@@ -148,6 +148,35 @@ def create_app() -> FastAPI:
|
||||
metrics_app = make_asgi_app()
|
||||
app.mount("/metrics", metrics_app)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""Handle all unhandled exceptions with structured error responses."""
|
||||
request_id = request.headers.get("X-Request-ID")
|
||||
logger.error(f"Unhandled exception: {exc}", extra={
|
||||
"request_id": request_id,
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"error_type": type(exc).__name__
|
||||
})
|
||||
|
||||
error_response = ErrorResponse(
|
||||
error={
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"message": "An unexpected error occurred",
|
||||
"status": 500,
|
||||
"details": [{
|
||||
"field": "internal",
|
||||
"message": str(exc),
|
||||
"code": type(exc).__name__
|
||||
}]
|
||||
},
|
||||
request_id=request_id
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=error_response.model_dump()
|
||||
)
|
||||
|
||||
@app.exception_handler(AITBCError)
|
||||
async def aitbc_error_handler(request: Request, exc: AITBCError) -> JSONResponse:
|
||||
"""Handle AITBC exceptions with structured error responses."""
|
||||
@@ -162,6 +191,13 @@ def create_app() -> FastAPI:
|
||||
async def validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
"""Handle FastAPI validation errors with structured error responses."""
|
||||
request_id = request.headers.get("X-Request-ID")
|
||||
logger.warning(f"Validation error: {exc}", extra={
|
||||
"request_id": request_id,
|
||||
"path": request.url.path,
|
||||
"method": request.method,
|
||||
"validation_errors": exc.errors()
|
||||
})
|
||||
|
||||
details = []
|
||||
for error in exc.errors():
|
||||
details.append({
|
||||
|
||||
@@ -3,14 +3,17 @@ Bitcoin Exchange Router for AITBC
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks, Request
|
||||
import uuid
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from aitbc.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
from ..schemas import (
|
||||
ExchangePaymentRequest,
|
||||
@@ -38,30 +41,32 @@ BITCOIN_CONFIG = {
|
||||
}
|
||||
|
||||
@router.post("/exchange/create-payment", response_model=ExchangePaymentResponse)
|
||||
@limiter.limit("20/minute")
|
||||
async def create_payment(
|
||||
request: ExchangePaymentRequest,
|
||||
request: Request,
|
||||
payment_request: ExchangePaymentRequest,
|
||||
background_tasks: BackgroundTasks
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new Bitcoin payment request"""
|
||||
|
||||
# Validate request
|
||||
if request.aitbc_amount <= 0 or request.btc_amount <= 0:
|
||||
if payment_request.aitbc_amount <= 0 or payment_request.btc_amount <= 0:
|
||||
raise HTTPException(status_code=400, detail="Invalid amount")
|
||||
|
||||
# Calculate expected BTC amount
|
||||
expected_btc = request.aitbc_amount / BITCOIN_CONFIG['exchange_rate']
|
||||
expected_btc = payment_request.aitbc_amount / BITCOIN_CONFIG['exchange_rate']
|
||||
|
||||
# Allow small difference for rounding
|
||||
if abs(request.btc_amount - expected_btc) > 0.00000001:
|
||||
if abs(payment_request.btc_amount - expected_btc) > 0.00000001:
|
||||
raise HTTPException(status_code=400, detail="Amount mismatch")
|
||||
|
||||
# Create payment record
|
||||
payment_id = str(uuid.uuid4())
|
||||
payment = {
|
||||
'payment_id': payment_id,
|
||||
'user_id': request.user_id,
|
||||
'aitbc_amount': request.aitbc_amount,
|
||||
'btc_amount': request.btc_amount,
|
||||
'user_id': payment_request.user_id,
|
||||
'aitbc_amount': payment_request.aitbc_amount,
|
||||
'btc_amount': payment_request.btc_amount,
|
||||
'payment_address': BITCOIN_CONFIG['main_address'],
|
||||
'status': 'pending',
|
||||
'created_at': int(time.time()),
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi import status as http_status
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
from ..schemas import MarketplaceBidRequest, MarketplaceOfferView, MarketplaceStatsView, MarketplaceBidView
|
||||
from ..services import MarketplaceService
|
||||
from ..storage import SessionDep
|
||||
from ..metrics import marketplace_requests_total, marketplace_errors_total
|
||||
from aitbc.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
router = APIRouter(tags=["marketplace"])
|
||||
|
||||
|
||||
@@ -20,7 +25,9 @@ def _get_service(session: SessionDep) -> MarketplaceService:
|
||||
response_model=list[MarketplaceOfferView],
|
||||
summary="List marketplace offers",
|
||||
)
|
||||
@limiter.limit("100/minute")
|
||||
async def list_marketplace_offers(
|
||||
request: Request,
|
||||
*,
|
||||
session: SessionDep,
|
||||
status_filter: str | None = Query(default=None, alias="status", description="Filter by offer status"),
|
||||
@@ -44,7 +51,12 @@ async def list_marketplace_offers(
|
||||
response_model=MarketplaceStatsView,
|
||||
summary="Get marketplace summary statistics",
|
||||
)
|
||||
async def get_marketplace_stats(*, session: SessionDep) -> MarketplaceStatsView:
|
||||
@limiter.limit("50/minute")
|
||||
async def get_marketplace_stats(
|
||||
request: Request,
|
||||
*,
|
||||
session: SessionDep
|
||||
) -> MarketplaceStatsView:
|
||||
marketplace_requests_total.labels(endpoint="/marketplace/stats", method="GET").inc()
|
||||
service = _get_service(session)
|
||||
try:
|
||||
@@ -59,7 +71,9 @@ async def get_marketplace_stats(*, session: SessionDep) -> MarketplaceStatsView:
|
||||
status_code=http_status.HTTP_202_ACCEPTED,
|
||||
summary="Submit a marketplace bid",
|
||||
)
|
||||
@limiter.limit("30/minute")
|
||||
async def submit_marketplace_bid(
|
||||
request: Request,
|
||||
payload: MarketplaceBidRequest,
|
||||
session: SessionDep,
|
||||
) -> dict[str, str]:
|
||||
|
||||
Reference in New Issue
Block a user