diff --git a/app/main.py b/app/main.py index 40b327c..833821c 100644 --- a/app/main.py +++ b/app/main.py @@ -180,6 +180,25 @@ async def radio_disconnected_handler(request: Request, exc: RadioDisconnectedErr return JSONResponse(status_code=503, content={"detail": "Radio not connected"}) +@app.middleware("http") +async def log_server_errors(request: Request, call_next): + """Capture 5xx errors and unhandled exceptions into the log ring buffer. + + Starlette writes unhandled-exception tracebacks to stderr, bypassing + Python logging, so they never reach the debug dump. This middleware + catches them and logs via ``logger.exception()`` so the full traceback + is preserved in the ring buffer for the ``GET /api/debug`` snapshot. + """ + try: + response = await call_next(request) + except Exception: + logger.exception("Unhandled exception on %s %s", request.method, request.url.path) + raise + if response.status_code >= 500: + logger.error("HTTP %d on %s %s", response.status_code, request.method, request.url.path) + return response + + # API routes - all prefixed with /api for production compatibility app.include_router(health.router, prefix="/api") app.include_router(debug.router, prefix="/api") diff --git a/app/services/message_send.py b/app/services/message_send.py index 6752b41..0789600 100644 --- a/app/services/message_send.py +++ b/app/services/message_send.py @@ -258,6 +258,12 @@ async def send_channel_message_with_effective_scope( ) raise HTTPException(status_code=504, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL) if send_result.type == EventType.ERROR: + logger.error( + "Radio returned error during %s for channel %s: %s", + action_label, + channel.name, + send_result.payload, + ) radio_manager.invalidate_cached_channel_slot(channel_key) else: radio_manager.note_channel_slot_used(channel_key)