feat: add proper HTTP status codes to all API error responses

- 217 error returns across 18 route files + api.py now use JSONResponse
  with appropriate HTTP status codes instead of returning HTTP 200
- Status code distribution: 500 (121), 400 (39), 503 (28), 404 (24), 409 (3), 502 (2)
- Fixed language.py tuple-return bug (was serializing as JSON array)
- Fixed bare except clauses in bipolar_mode.py and voice.py
- Body-level error schemas preserved (status/error + success/error patterns)
  so web UI continues working without changes
- chat.py (SSE) unchanged: errors sent within stream protocol
- All 170 tests pass
This commit is contained in:
2026-04-15 15:43:18 +03:00
parent 33b2033cc3
commit edc9f27925
19 changed files with 243 additions and 227 deletions

View File

@@ -3,7 +3,7 @@
import os
from typing import List
from fastapi import APIRouter, UploadFile, File, Form
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
import globals
from routes.models import (
ManualCropRequest, DescriptionUpdateRequest,
@@ -25,7 +25,7 @@ async def trigger_profile_picture_change(
):
"""Change Miku's profile picture. If a file is provided, use it. Otherwise, search Danbooru."""
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
return {"status": "error", "message": "Bot not ready"}
return JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"})
try:
from utils.profile_picture_manager import profile_picture_manager
@@ -54,17 +54,17 @@ async def trigger_profile_picture_change(
"metadata": result.get("metadata", {})
}
else:
return {
return JSONResponse(status_code=500, content={
"status": "error",
"message": result.get("error", "Unknown error"),
"source": result["source"]
}
})
except Exception as e:
logger.error(f"Error in profile picture API: {e}")
import traceback
traceback.print_exc()
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
return JSONResponse(status_code=500, content={"status": "error", "message": f"Unexpected error: {str(e)}"})
@router.get("/profile-picture/metadata")
@@ -78,14 +78,14 @@ async def get_profile_picture_metadata():
else:
return {"status": "ok", "metadata": None, "message": "No metadata found"}
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/restore-fallback")
async def restore_fallback_profile_picture():
"""Restore the original fallback profile picture"""
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
return {"status": "error", "message": "Bot not ready"}
return JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"})
try:
from utils.profile_picture_manager import profile_picture_manager
@@ -93,16 +93,16 @@ async def restore_fallback_profile_picture():
if success:
return {"status": "ok", "message": "Fallback profile picture restored"}
else:
return {"status": "error", "message": "Failed to restore fallback"}
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to restore fallback"})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/role-color/custom")
async def set_custom_role_color(hex_color: str = Form(...)):
"""Set a custom role color across all servers"""
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
return {"status": "error", "message": "Bot not ready"}
return JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"})
try:
from utils.profile_picture_manager import profile_picture_manager
@@ -114,16 +114,16 @@ async def set_custom_role_color(hex_color: str = Form(...)):
"color": result["color"]
}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/role-color/reset-fallback")
async def reset_role_color_to_fallback():
"""Reset role color to fallback (#86cecb)"""
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
return {"status": "error", "message": "Bot not ready"}
return JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"})
try:
from utils.profile_picture_manager import profile_picture_manager
@@ -135,9 +135,9 @@ async def reset_role_color_to_fallback():
"color": result["color"]
}
else:
return {"status": "error", "message": "Failed to reset color"}
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to reset color"})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
# ========== Profile Picture — Image Serving ==========
@@ -148,7 +148,7 @@ async def serve_original_profile_picture():
from utils.profile_picture_manager import profile_picture_manager
path = profile_picture_manager.ORIGINAL_PATH
if not os.path.exists(path):
return {"status": "error", "message": "No original image found"}
return JSONResponse(status_code=404, content={"status": "error", "message": "No original image found"})
return FileResponse(path, media_type="image/png", headers={"Cache-Control": "no-cache"})
@@ -158,7 +158,7 @@ async def serve_current_profile_picture():
from utils.profile_picture_manager import profile_picture_manager
path = profile_picture_manager.CURRENT_PATH
if not os.path.exists(path):
return {"status": "error", "message": "No current image found"}
return JSONResponse(status_code=404, content={"status": "error", "message": "No current image found"})
return FileResponse(path, media_type="image/png", headers={"Cache-Control": "no-cache"})
@@ -171,7 +171,7 @@ async def trigger_profile_picture_change_no_crop(
):
"""Change Miku's profile picture but skip auto-cropping."""
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
return {"status": "error", "message": "Bot not ready"}
return JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"})
try:
from utils.profile_picture_manager import profile_picture_manager
@@ -200,23 +200,23 @@ async def trigger_profile_picture_change_no_crop(
"metadata": result.get("metadata", {})
}
else:
return {
return JSONResponse(status_code=500, content={
"status": "error",
"message": result.get("error", "Unknown error"),
"source": result.get("source")
}
})
except Exception as e:
logger.error(f"Error in change-no-crop API: {e}")
import traceback
traceback.print_exc()
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
return JSONResponse(status_code=500, content={"status": "error", "message": f"Unexpected error: {str(e)}"})
@router.post("/profile-picture/manual-crop")
async def apply_manual_crop(req: ManualCropRequest):
"""Apply a manual crop to the stored original image"""
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
return {"status": "error", "message": "Bot not ready"}
return JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"})
try:
from utils.profile_picture_manager import profile_picture_manager
@@ -230,16 +230,16 @@ async def apply_manual_crop(req: ManualCropRequest):
"metadata": result.get("metadata", {})
}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/auto-crop")
async def apply_auto_crop():
"""Run intelligent auto-crop on the stored original image"""
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
return {"status": "error", "message": "Bot not ready"}
return JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"})
try:
from utils.profile_picture_manager import profile_picture_manager
@@ -251,9 +251,9 @@ async def apply_auto_crop():
"metadata": result.get("metadata", {})
}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/description")
@@ -267,9 +267,9 @@ async def update_profile_picture_description(req: DescriptionUpdateRequest):
if result["success"]:
return {"status": "ok", "message": "Description updated successfully"}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/regenerate-description")
@@ -285,9 +285,9 @@ async def regenerate_profile_picture_description():
"description": result["description"]
}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.get("/profile-picture/description")
@@ -298,7 +298,7 @@ async def get_profile_picture_description():
description = profile_picture_manager.get_current_description()
return {"status": "ok", "description": description or ""}
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
# ========== Profile Picture — Album / Gallery ==========
@@ -311,7 +311,7 @@ async def list_album_entries():
entries = profile_picture_manager.get_album_entries()
return {"status": "ok", "entries": entries, "count": len(entries)}
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.get("/profile-picture/album/disk-usage")
@@ -322,7 +322,7 @@ async def get_album_disk_usage():
usage = profile_picture_manager.get_album_disk_usage()
return {"status": "ok", **usage}
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.get("/profile-picture/album/{entry_id}")
@@ -334,25 +334,25 @@ async def get_album_entry(entry_id: str):
if meta:
return {"status": "ok", "entry": meta}
else:
return {"status": "error", "message": "Album entry not found"}
return JSONResponse(status_code=404, content={"status": "error", "message": "Album entry not found"})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.get("/profile-picture/album/{entry_id}/image/{image_type}")
async def serve_album_image(entry_id: str, image_type: str):
"""Serve an album entry's image (original or cropped)"""
if image_type not in ("original", "cropped"):
return {"status": "error", "message": "image_type must be 'original' or 'cropped'"}
return JSONResponse(status_code=400, content={"status": "error", "message": "image_type must be 'original' or 'cropped'"})
try:
from utils.profile_picture_manager import profile_picture_manager
path = profile_picture_manager.get_album_image_path(entry_id, image_type)
if path:
return FileResponse(path, media_type="image/png", headers={"Cache-Control": "no-cache"})
else:
return {"status": "error", "message": f"No {image_type} image for this entry"}
return JSONResponse(status_code=404, content={"status": "error", "message": f"No {image_type} image for this entry"})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/album/add")
@@ -373,10 +373,10 @@ async def add_to_album(file: UploadFile = File(...)):
"metadata": result.get("metadata", {})
}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
logger.error(f"Error adding to album: {e}")
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/album/add-batch")
@@ -403,14 +403,14 @@ async def add_batch_to_album(files: List[UploadFile] = File(...)):
}
except Exception as e:
logger.error(f"Error in batch album add: {e}")
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/album/{entry_id}/set-current")
async def set_album_entry_as_current(entry_id: str):
"""Set an album entry as the current Discord profile picture"""
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
return {"status": "error", "message": "Bot not ready"}
return JSONResponse(status_code=503, content={"status": "error", "message": "Bot not ready"})
try:
from utils.profile_picture_manager import profile_picture_manager
result = await profile_picture_manager.set_album_entry_as_current(
@@ -423,9 +423,9 @@ async def set_album_entry_as_current(entry_id: str):
"archived_entry_id": result.get("archived_entry_id")
}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/album/{entry_id}/manual-crop")
@@ -444,9 +444,9 @@ async def manual_crop_album_entry(entry_id: str, req: AlbumCropRequest):
"metadata": result.get("metadata", {})
}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/album/{entry_id}/auto-crop")
@@ -464,9 +464,9 @@ async def auto_crop_album_entry(entry_id: str):
"metadata": result.get("metadata", {})
}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/album/{entry_id}/description")
@@ -480,9 +480,9 @@ async def update_album_entry_description(entry_id: str, req: AlbumDescriptionReq
if result["success"]:
return {"status": "ok", "message": "Description updated"}
else:
return {"status": "error", "message": result.get("error", "Unknown error")}
return JSONResponse(status_code=500, content={"status": "error", "message": result.get("error", "Unknown error")})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.delete("/profile-picture/album/{entry_id}")
@@ -493,9 +493,9 @@ async def delete_album_entry(entry_id: str):
if profile_picture_manager.delete_album_entry(entry_id):
return {"status": "ok", "message": "Album entry deleted"}
else:
return {"status": "error", "message": "Album entry not found"}
return JSONResponse(status_code=404, content={"status": "error", "message": "Album entry not found"})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/album/delete-bulk")
@@ -510,7 +510,7 @@ async def bulk_delete_album_entries(req: BulkDeleteRequest):
**result
}
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
@router.post("/profile-picture/album/add-current")
@@ -522,6 +522,6 @@ async def add_current_to_album():
if entry_id:
return {"status": "ok", "message": "Current PFP archived to album", "entry_id": entry_id}
else:
return {"status": "error", "message": "No current PFP to archive"}
return JSONResponse(status_code=404, content={"status": "error", "message": "No current PFP to archive"})
except Exception as e:
return {"status": "error", "message": str(e)}
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})