Repository: privacera/paig (language: python)

Issue:
[ENH]: Improve API Error Handling for Wrong API Endpoints
### Contact Details

_No response_

### Feature Description

Currently, when a user hits an incorrect API endpoint, the error response is not as informative as it could be. To improve the developer experience and make debugging easier, we propose enhancing the response for wrong API endpoints as below.

Here's an example of a well-structured response:

Example - GET 'http://localhost:9090/api/group'
Response -
`{
    "timestamp": "2024-09-16T11:21:21.470+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/api/group"
}`

Key components -
1. HTTP Status Code: 404 Not Found – clearly indicating the issue.
2. Path: Showing the invalid endpoint requested.

This helps the client understand the problem and provides them with the information they need to fix the issue.

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion / hints:

Fix patch:
diff --git a/paig-server/backend/paig/server.py b/paig-server/backend/paig/server.py
index bb915e4b..c2d9700d 100644
--- a/paig-server/backend/paig/server.py
+++ b/paig-server/backend/paig/server.py
@@ -23,6 +23,7 @@
 import logging
 from fastapi.exceptions import RequestValidationError
 from fastapi.encoders import jsonable_encoder
+from starlette.exceptions import HTTPException as StarletteHTTPException
 
 logger = logging.getLogger(__name__)
 
@@ -102,6 +103,16 @@ async def unhandled_exception_handler(request: Request, exc: Exception):
                      "message": "An unexpected error occurred. Please try again later."}
         )
 
+    @app_.exception_handler(StarletteHTTPException)
+    async def path_not_found_exception_handler(request: Request, exc: StarletteHTTPException):
+        if exc.status_code == 404:
+            return JSONResponse(content=jsonable_encoder({
+                    "error_code": 404,
+                    "success": False,
+                    "message": "Path Not Found",
+                    "path": request.url.path
+                }), status_code=status.HTTP_404_NOT_FOUND)
+
 
 def init_cache() -> None:
     pass


Test patch:
diff --git a/paig-server/backend/paig/tests/api/router/test_path_not_found.py b/paig-server/backend/paig/tests/api/router/test_path_not_found.py
new file mode 100644
index 00000000..8f9ae3e8
--- /dev/null
+++ b/paig-server/backend/paig/tests/api/router/test_path_not_found.py
@@ -0,0 +1,46 @@
+import pytest
+from fastapi import FastAPI
+from httpx import AsyncClient
+
+from core.security.authentication import get_auth_user
+
+user_services_base_route = "account-service/api"
+governance_services_base_route = "governance-service/api/ai"
+
+
+class TestPathNotFound:
+    def setup_method(self):
+        self.auth_user_obj = {
+            "id": 1,
+            "username": "test",
+            "roles": ["USER"]
+        }
+
+    def auth_user(self):
+        return self.auth_user_obj
+
+    @pytest.mark.asyncio
+    async def test_invalid_path_account_service_api(self, client: AsyncClient, app: FastAPI):
+        app.dependency_overrides[get_auth_user] = self.auth_user
+
+        response = await client.post(
+            f"{user_services_base_route}/invalid_path"
+        )
+
+        assert response.status_code == 404
+        assert response.json()['error_code'] == 404
+        assert response.json()['message'] == 'Path Not Found'
+        assert (response.json()['path'] == f"/{user_services_base_route}/invalid_path")
+
+    @pytest.mark.asyncio
+    async def test_invalid_path_governance_service_api(self, client: AsyncClient, app: FastAPI):
+        app.dependency_overrides[get_auth_user] = self.auth_user
+
+        response = await client.get(
+            f"{governance_services_base_route}/invalid_path"
+        )
+
+        assert response.status_code == 404
+        assert response.json()['error_code'] == 404
+        assert response.json()['message'] == 'Path Not Found'
+        assert (response.json()['path'] == f"/{governance_services_base_route}/invalid_path")