From 74724973d852074c321528b4656ac4361654ce4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:18:42 +0000 Subject: [PATCH] Fix API routes to return JSON instead of HTML on errors - Modified exception handlers in app/main.py to check if request path starts with /api/ - HTTPException handler returns JSON for API routes, HTML for frontend routes - General exception handler (500) also checks and returns appropriate format - Enhanced frontend deleteFile() to handle non-JSON responses gracefully - Added content-type checking before parsing JSON - Added comprehensive tests for API error handling Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/main.py | 48 +++++++++-- frontend/templates/files.html | 17 +++- tests/test_api_error_handling.py | 137 +++++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 tests/test_api_error_handling.py diff --git a/app/main.py b/app/main.py index a5198f7f..6195838f 100644 --- a/app/main.py +++ b/app/main.py @@ -4,6 +4,7 @@ import logging import pathlib from fastapi import FastAPI, HTTPException, Request, status +from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from starlette.middleware.sessions import SessionMiddleware @@ -92,21 +93,54 @@ async def shutdown_event(): # Send shutdown notification notify_shutdown() -# Custom 404 - we can still return the Jinja2 template, or the old static file: -@app.exception_handler(404) -async def custom_404_handler(request: Request, exc: HTTPException): - # Serve the 404 template directly +# Custom exception handlers that return JSON for API routes and HTML for frontend routes +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """ + Handle all HTTPException instances. + Returns JSON for API routes, HTML templates for frontend routes. + """ + # For API routes, always return JSON + if request.url.path.startswith("/api/"): + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail} + ) + + # For frontend routes, return appropriate HTML templates templates = Jinja2Templates(directory=str(static_dir.parent / "templates")) + + # Handle 404 errors with a custom template + if exc.status_code == 404: + return templates.TemplateResponse( + "404.html", + {"request": request}, + status_code=status.HTTP_404_NOT_FOUND + ) + + # For other HTTP errors, we could create specific templates or use a generic one + # For now, return a simple error page return templates.TemplateResponse( - "404.html", + "404.html", # Reuse 404 template for other errors, or create a generic error template {"request": request}, - status_code=status.HTTP_404_NOT_FOUND + status_code=exc.status_code ) @app.exception_handler(500) async def custom_500_handler(request: Request, exc: Exception): + """ + Handle internal server errors (500). + Returns JSON for API routes, HTML templates for frontend routes. + """ + # For API routes, return JSON instead of HTML + if request.url.path.startswith("/api/"): + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"detail": "Internal server error"} + ) + + # Serve the 500 template for non-API routes templates = Jinja2Templates(directory=str(static_dir.parent / "templates")) - # Option 1: Keep it simple, just show a funny 500 message: return templates.TemplateResponse( "500.html", {"request": request, "exc": exc}, diff --git a/frontend/templates/files.html b/frontend/templates/files.html index 7b01274c..6d6d0560 100644 --- a/frontend/templates/files.html +++ b/frontend/templates/files.html @@ -501,9 +501,20 @@ }) .then(response => { if (!response.ok) { - return response.json().then(err => { - throw new Error(err.detail || 'Failed to delete file'); - }); + // Try to parse JSON error response, but handle non-JSON gracefully + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return response.json().then(err => { + throw new Error(err.detail || 'Failed to delete file'); + }); + } else { + // Non-JSON response (likely HTML error page) + return response.text().then(text => { + // Extract a readable error message + const statusText = response.statusText || 'Error'; + throw new Error(`${statusText} (${response.status}): Server returned non-JSON response`); + }); + } } return response.json(); }) diff --git a/tests/test_api_error_handling.py b/tests/test_api_error_handling.py new file mode 100644 index 00000000..af3e96c2 --- /dev/null +++ b/tests/test_api_error_handling.py @@ -0,0 +1,137 @@ +""" +Tests for API error handling - ensuring JSON responses for API routes. +""" + +import pytest +from fastapi.testclient import TestClient +from app.models import FileRecord +from unittest.mock import patch + + +@pytest.mark.integration +@pytest.mark.requires_db +class TestAPIErrorHandling: + """Tests for API error responses - ensuring they return JSON, not HTML.""" + + def test_delete_nonexistent_file_returns_json_404(self, client: TestClient): + """Test that deleting a non-existent file returns JSON 404, not HTML.""" + response = client.delete("/api/files/99999") + + # Should return 404 + assert response.status_code == 404 + + # Should be JSON, not HTML + content_type = response.headers.get("content-type", "") + assert "application/json" in content_type, f"Expected JSON but got {content_type}" + + # Should not contain HTML + assert not response.text.startswith("