diff --git a/src/ahriman/web/middlewares/exception_handler.py b/src/ahriman/web/middlewares/exception_handler.py
index 342c2fc7..e7a6b3bf 100644
--- a/src/ahriman/web/middlewares/exception_handler.py
+++ b/src/ahriman/web/middlewares/exception_handler.py
@@ -18,8 +18,8 @@
# along with this program. If not, see .
#
from aiohttp.web import middleware, Request
-from aiohttp.web_exceptions import HTTPException
-from aiohttp.web_response import StreamResponse
+from aiohttp.web_exceptions import HTTPClientError, HTTPException, HTTPServerError
+from aiohttp.web_response import json_response, StreamResponse
from logging import Logger
from ahriman.web.middlewares import HandlerType, MiddlewareType
@@ -35,10 +35,15 @@ def exception_handler(logger: Logger) -> MiddlewareType:
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
try:
return await handler(request)
+ except HTTPClientError as e:
+ return json_response(data={"error": e.reason}, status=e.status_code)
+ except HTTPServerError as e:
+ logger.exception("server exception during performing request to %s", request.path)
+ return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPException:
- raise # we do not raise 5xx exceptions actually so it should be fine
- except Exception:
- logger.exception("exception during performing request to %s", request.path)
- raise
+ raise # just raise 2xx and 3xx codes
+ except Exception as e:
+ logger.exception("unknown exception during performing request to %s", request.path)
+ return json_response(data={"error": str(e)}, status=500)
return handle
diff --git a/src/ahriman/web/views/service/add.py b/src/ahriman/web/views/service/add.py
index 63a52426..83168c44 100644
--- a/src/ahriman/web/views/service/add.py
+++ b/src/ahriman/web/views/service/add.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPFound, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPFound, Response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@@ -42,12 +42,11 @@ class AddView(BaseView):
:return: redirect to main page on success
"""
- data = await self.extract_data(["packages"])
-
try:
+ data = await self.extract_data(["packages"])
packages = data["packages"]
except Exception as e:
- return json_response(data=str(e), status=400)
+ raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=True)
diff --git a/src/ahriman/web/views/service/remove.py b/src/ahriman/web/views/service/remove.py
index cbbf2ed6..19bb3b00 100644
--- a/src/ahriman/web/views/service/remove.py
+++ b/src/ahriman/web/views/service/remove.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPFound, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPFound, Response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@@ -42,12 +42,11 @@ class RemoveView(BaseView):
:return: redirect to main page on success
"""
- data = await self.extract_data(["packages"])
-
try:
+ data = await self.extract_data(["packages"])
packages = data["packages"]
except Exception as e:
- return json_response(data=str(e), status=400)
+ raise HTTPBadRequest(reason=str(e))
self.spawner.packages_remove(packages)
diff --git a/src/ahriman/web/views/service/request.py b/src/ahriman/web/views/service/request.py
index 611ca7fa..340e9f73 100644
--- a/src/ahriman/web/views/service/request.py
+++ b/src/ahriman/web/views/service/request.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPFound, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPFound, Response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@@ -42,12 +42,11 @@ class RequestView(BaseView):
:return: redirect to main page on success
"""
- data = await self.extract_data(["packages"])
-
try:
+ data = await self.extract_data(["packages"])
packages = data["packages"]
except Exception as e:
- return json_response(data=str(e), status=400)
+ raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=False)
diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py
index d53c84e6..863f021d 100644
--- a/src/ahriman/web/views/service/search.py
+++ b/src/ahriman/web/views/service/search.py
@@ -19,7 +19,7 @@
#
import aur # type: ignore
-from aiohttp.web import Response, json_response
+from aiohttp.web import HTTPBadRequest, Response, json_response
from typing import Callable, Iterator
from ahriman.models.user_access import UserAccess
@@ -47,7 +47,7 @@ class SearchView(BaseView):
search_string = " ".join(search)
if not search_string:
- return json_response(data="Search string must not be empty", status=400)
+ raise HTTPBadRequest(reason="Search string must not be empty")
packages = aur.search(search_string)
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
diff --git a/src/ahriman/web/views/status/ahriman.py b/src/ahriman/web/views/status/ahriman.py
index 40e0d101..f9963850 100644
--- a/src/ahriman/web/views/status/ahriman.py
+++ b/src/ahriman/web/views/status/ahriman.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.user_access import UserAccess
@@ -53,12 +53,11 @@ class AhrimanView(BaseView):
:return: 204 on success
"""
- data = await self.extract_data()
-
try:
+ data = await self.extract_data()
status = BuildStatusEnum(data["status"])
except Exception as e:
- return json_response(data=str(e), status=400)
+ raise HTTPBadRequest(reason=str(e))
self.service.update_self(status)
diff --git a/src/ahriman/web/views/status/package.py b/src/ahriman/web/views/status/package.py
index ad814481..1afcb757 100644
--- a/src/ahriman/web/views/status/package.py
+++ b/src/ahriman/web/views/status/package.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackage
from ahriman.models.build_status import BuildStatusEnum
@@ -88,11 +88,11 @@ class PackageView(BaseView):
package = Package.from_json(data["package"]) if "package" in data else None
status = BuildStatusEnum(data["status"])
except Exception as e:
- return json_response(data=str(e), status=400)
+ raise HTTPBadRequest(reason=str(e))
try:
self.service.update(base, status, package)
except UnknownPackage:
- return json_response(data=f"Package {base} is unknown, but no package body set", status=400)
+ raise HTTPBadRequest(reason=f"Package {base} is unknown, but no package body set")
raise HTTPNoContent()
diff --git a/tests/ahriman/web/middlewares/test_exception_handler.py b/tests/ahriman/web/middlewares/test_exception_handler.py
index ea281093..feb36966 100644
--- a/tests/ahriman/web/middlewares/test_exception_handler.py
+++ b/tests/ahriman/web/middlewares/test_exception_handler.py
@@ -1,13 +1,24 @@
+import json
import logging
import pytest
-from aiohttp.web_exceptions import HTTPBadRequest, HTTPNoContent
+from aiohttp.web_exceptions import HTTPBadRequest, HTTPInternalServerError, HTTPNoContent
from pytest_mock import MockerFixture
+from typing import Any
from unittest.mock import AsyncMock
from ahriman.web.middlewares.exception_handler import exception_handler
+def _extract_body(response: Any) -> Any:
+ """
+ extract json body from given object
+ :param response: response (any actually) object
+ :return: body key from the object converted to json
+ """
+ return json.loads(getattr(response, "body"))
+
+
async def test_exception_handler(mocker: MockerFixture) -> None:
"""
must pass success response
@@ -23,7 +34,7 @@ async def test_exception_handler(mocker: MockerFixture) -> None:
async def test_exception_handler_success(mocker: MockerFixture) -> None:
"""
- must pass client exception
+ must pass 2xx and 3xx codes
"""
request = pytest.helpers.request("", "", "")
request_handler = AsyncMock(side_effect=HTTPNoContent())
@@ -37,27 +48,41 @@ async def test_exception_handler_success(mocker: MockerFixture) -> None:
async def test_exception_handler_client_error(mocker: MockerFixture) -> None:
"""
- must pass client exception
+ must handle client exception
"""
request = pytest.helpers.request("", "", "")
request_handler = AsyncMock(side_effect=HTTPBadRequest())
logging_mock = mocker.patch("logging.Logger.exception")
handler = exception_handler(logging.getLogger())
- with pytest.raises(HTTPBadRequest):
- await handler(request, request_handler)
+ response = await handler(request, request_handler)
+ assert _extract_body(response) == {"error": HTTPBadRequest().reason}
logging_mock.assert_not_called()
async def test_exception_handler_server_error(mocker: MockerFixture) -> None:
"""
- must log server exception and re-raise it
+ must handle server exception
"""
request = pytest.helpers.request("", "", "")
- request_handler = AsyncMock(side_effect=Exception())
+ request_handler = AsyncMock(side_effect=HTTPInternalServerError())
logging_mock = mocker.patch("logging.Logger.exception")
handler = exception_handler(logging.getLogger())
- with pytest.raises(Exception):
- await handler(request, request_handler)
+ response = await handler(request, request_handler)
+ assert _extract_body(response) == {"error": HTTPInternalServerError().reason}
+ logging_mock.assert_called_once()
+
+
+async def test_exception_handler_unknown_error(mocker: MockerFixture) -> None:
+ """
+ must log server exception and re-raise it
+ """
+ request = pytest.helpers.request("", "", "")
+ request_handler = AsyncMock(side_effect=Exception("An error"))
+ logging_mock = mocker.patch("logging.Logger.exception")
+
+ handler = exception_handler(logging.getLogger())
+ response = await handler(request, request_handler)
+ assert _extract_body(response) == {"error": "An error"}
logging_mock.assert_called_once()