Skip to content

Commit

Permalink
Merge pull request #331 from ral-facilities/error-signalling
Browse files Browse the repository at this point in the history
Search API Error Signalling/Formatting
  • Loading branch information
MRichards99 authored Feb 16, 2022
2 parents 11d59e8 + dd94c1a commit aefafcc
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 4 deletions.
8 changes: 7 additions & 1 deletion datagateway_api/src/api_start_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path

from apispec import APISpec
from flask import Response
from flask_cors import CORS
from flask_restful import Api
from flask_swagger_ui import get_swaggerui_blueprint
Expand Down Expand Up @@ -55,7 +56,12 @@ class CustomErrorHandledApi(Api):
"""

def handle_error(self, e):
return str(e), e.status_code
if isinstance(e.args[0], (str, dict, tuple, Response)):
error_msg = e.args[0]
else:
error_msg = str(e)

return error_msg, e.status_code


def create_app_infrastructure(flask_app):
Expand Down
6 changes: 6 additions & 0 deletions datagateway_api/src/resources/search_api_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
get_files_count,
get_search,
get_with_pid,
search_api_error_handling,
)

log = logging.getLogger()
Expand All @@ -26,6 +27,7 @@ def get_search_endpoint(entity_name):
"""

class Endpoint(Resource):
@search_api_error_handling
def get(self):
filters = get_filters_from_query_string("search_api", entity_name)
log.debug("Filters: %s", filters)
Expand All @@ -49,6 +51,7 @@ def get_single_endpoint(entity_name):
"""

class EndpointWithID(Resource):
@search_api_error_handling
def get(self, pid):
filters = get_filters_from_query_string("search_api", entity_name)
log.debug("Filters: %s", filters)
Expand All @@ -72,6 +75,7 @@ def get_number_count_endpoint(entity_name):
"""

class CountEndpoint(Resource):
@search_api_error_handling
def get(self):
# Only WHERE included on count endpoints
filters = get_filters_from_query_string("search_api", entity_name)
Expand All @@ -96,6 +100,7 @@ def get_files_endpoint(entity_name):
"""

class FilesEndpoint(Resource):
@search_api_error_handling
def get(self, pid):
filters = get_filters_from_query_string("search_api", entity_name)
log.debug("Filters: %s", filters)
Expand All @@ -120,6 +125,7 @@ def get_number_count_files_endpoint(entity_name):
"""

class CountFilesEndpoint(Resource):
@search_api_error_handling
def get(self, pid):
# Only WHERE included on count endpoints
filters = get_filters_from_query_string("search_api", entity_name)
Expand Down
46 changes: 45 additions & 1 deletion datagateway_api/src/search_api/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from functools import wraps
import json
import logging

from datagateway_api.src.common.exceptions import MissingRecordError
from datagateway_api.src.common.exceptions import (
BadRequestError,
MissingRecordError,
)
from datagateway_api.src.common.filter_order_handler import FilterOrderHandler
from datagateway_api.src.search_api.filters import (
SearchAPIIncludeFilter,
Expand All @@ -18,6 +22,46 @@
log = logging.getLogger()


def search_api_error_handling(method):
"""
Decorator (similar to `queries_records`) to handle exceptions and present in a way
required for the search API. The decorator should be applied to search API endpoint
resources
:param method: The method for the endpoint
:raises: Any exception caught by the execution of `method`
"""

@wraps(method)
def wrapper_error_handling(*args, **kwargs):
try:
return method(*args, **kwargs)
except (ValueError, TypeError, AttributeError) as e:
log.exception(msg=e.args)
raise BadRequestError(create_error_message(BadRequestError()))
except Exception as e:
log.exception(msg=e.args)
try:
e.status_code
except AttributeError:
# If no status code exists (for non-API defined exceptions), defensively
# assign a 500
e.status_code = 500

raise type(e)(create_error_message(e))

def create_error_message(e):
return {
"error": {
"statusCode": e.status_code,
"name": e.__class__.__name__,
"message": str(e),
},
}

return wrapper_error_handling


@client_manager
def get_search(entity_name, filters):
"""
Expand Down
42 changes: 42 additions & 0 deletions test/search_api/test_error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pytest

from datagateway_api.src.common.exceptions import (
BadRequestError,
FilterError,
MissingRecordError,
SearchAPIError,
)
from datagateway_api.src.search_api.helpers import search_api_error_handling


class TestErrorHandling:
@pytest.mark.parametrize(
"raised_exception, expected_exception, status_code",
[
pytest.param(BadRequestError, BadRequestError, 400, id="Bad request error"),
pytest.param(FilterError, FilterError, 400, id="Invalid filter"),
pytest.param(
MissingRecordError, MissingRecordError, 404, id="Missing record",
),
pytest.param(SearchAPIError, SearchAPIError, 500, id="Search API error"),
pytest.param(TypeError, BadRequestError, 400, id="Type error"),
pytest.param(ValueError, BadRequestError, 400, id="Value error"),
pytest.param(AttributeError, BadRequestError, 400, id="Attribute error"),
],
)
def test_valid_error_raised(
self, raised_exception, expected_exception, status_code,
):
@search_api_error_handling
def raise_exception():
raise raised_exception()

try:
raise_exception()
except Exception as e:
assert isinstance(e.args[0], dict)
assert e.status_code == status_code
assert list(e.args[0]["error"].keys()) == ["statusCode", "name", "message"]

with pytest.raises(expected_exception):
raise_exception()
7 changes: 5 additions & 2 deletions test/test_queries_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ def test_valid_error_raised(
def raise_exception():
raise raised_exception()

with pytest.raises(expected_exception) as ctx:
try:
raise_exception()
except Exception as e:
assert e.status_code == status_code

assert ctx.exception.status_code == status_code
with pytest.raises(expected_exception):
raise_exception()

0 comments on commit aefafcc

Please sign in to comment.