-
Notifications
You must be signed in to change notification settings - Fork 301
Issue 431: return all exceptions as JSON:API error objects #437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
import re | ||
|
||
from django.core.exceptions import FieldDoesNotExist, FieldError | ||
from django.utils.translation import ugettext_lazy as _ | ||
from rest_framework import exceptions, status | ||
from rest_framework.response import Response | ||
|
||
from rest_framework_json_api import utils | ||
|
||
|
@@ -14,6 +18,52 @@ def rendered_with_json_api(view): | |
return False | ||
|
||
|
||
def unhandled_drf_exception_handler(exc, context): | ||
""" | ||
Deal with exceptions that DRF doesn't catch and return a JSON:API errors object. | ||
|
||
For model query parameters, attempt to identify the parameter that caused the exception: | ||
"Cannot resolve keyword 'xname' into field. Choices are: name, title, ...." | ||
Unfortunately there's no "clean" way to identify which field caused the exception other than | ||
parsing the error message. If the parse fails, a more generic error is reported. | ||
|
||
Even a 500 error for a jsonapi view should return a jsonapi error object, not an HTML response. | ||
""" | ||
if not rendered_with_json_api(context['view']): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should move this check to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, will take a look. |
||
return None | ||
keymatch = re.compile(r"^Cannot resolve keyword '(?P<keyword>\w+)' into field.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be moved into the if as it is only needed there |
||
if isinstance(exc, (FieldError, FieldDoesNotExist,)): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure whether this is an 400 error, as it is not dependen on the request but on the actual code which is erroneousness. Specification says for 400 |
||
matched = keymatch.match(str(exc)) | ||
bad_kw = matched.group("keyword") if matched else "?" | ||
status_code = 400 | ||
errors = [ | ||
{ | ||
"status": status_code, | ||
"code": "field_error", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think we should assign a code. In the rest json api framework is not an application though but a framework so I think we shoulnd't set this but only the implementing application if desired. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rather err on the side of providing more information to the client (e.g. |
||
"title": "Field error", | ||
"detail": str(exc), | ||
"source": { | ||
"pointer": context["request"].path, | ||
"parameter": bad_kw | ||
} | ||
} | ||
] | ||
else: | ||
status_code = 500 | ||
errors = [ | ||
{ | ||
"status": status_code, | ||
"code": "server_error", | ||
"title": "Internal Server Error", | ||
"detail": str(exc), | ||
"source": { | ||
"pointer": context["request"].path, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A pointer should be a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See prior comment; When this is exception is raised as part of a viewset GET it is (maybe?) caused by an incorrect field name. |
||
} | ||
} | ||
] | ||
return Response(errors, status=status_code) | ||
|
||
|
||
def exception_handler(exc, context): | ||
# Import this here to avoid potential edge-case circular imports, which | ||
# crashes with: | ||
|
@@ -26,7 +76,7 @@ def exception_handler(exc, context): | |
# Render exception with DRF | ||
response = drf_exception_handler(exc, context) | ||
if not response: | ||
return response | ||
return unhandled_drf_exception_handler(exc, context) | ||
|
||
# Use regular DRF format if not rendered by DRF JSON API and not uniform | ||
is_json_api_view = rendered_with_json_api(context['view']) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For new tests I think we should use plain pytest and not unittest. In the long we should get rid of legacy unittest anyway.
Also question is why mocking is needed here. I think if we added BadBlogViewSet into the routing it could be simply called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was frankly confused by the 3-levels of tests/, tests/unit, tests/integration. Any kind of more detail in the contributing doc you could add would be appreciated.
I can move BadBlogViewSet into the example app views. I based the mocking approach on other projects I've seen where the tests are self-contained in the test code.