-
-
Notifications
You must be signed in to change notification settings - Fork 316
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
Handle error consistently on the backend using custom drf error handler #909
Conversation
`previews` api route uses the context manager to capture error and convert it to proper error response
…exception as first argument similar to when calling default parser
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.
@silentninja Please see code comments.
I'm also confused about why we're not using DRF's custom exception handler. https://www.django-rest-framework.org/api-guide/exceptions/. You mentioned in the PR description that you'd have to do it on a per-view basis, but I'm not sure why that's the case. You can create an exception handler in a single place and then update our settings file to use it as the default exception handler. This seems cleanest to me - we can then use standard Python/DRF exceptions without as much boilerplate.
See this Stack Overflow answer for some more discussion on this topic.
mathesar/errors.py
Outdated
class InvalidTableError(Exception): | ||
pass | ||
|
||
|
||
ExceptionTransformerDetail = namedtuple( |
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.
@silentninja I updated the whitespace here to make it more easily readable (in my opinion) and to match the rest of the code style. I figured updating it directly would be easier.
mathesar/api/viewsets/tables.py
Outdated
@@ -105,18 +107,21 @@ def previews(self, request, pk=None): | |||
raise ValidationError("Incorrect number of columns in request.") | |||
|
|||
table_data = TableSerializer(table, context={"request": request}).data | |||
try: | |||
with exception_transformer({**dict.fromkeys([DataError, IntegrityError], ExceptionTransformerDetail(400, |
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.
@silentninja This function call for exception_transformer
is a little hard to follow. It would be clearer if we could declare the dictionaries in a separate line and pass them here.
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 also think it would be clearer to instantiate the named tuple using keyword arguments (e.g. status_code=400
, etc.) so that we don't have to remember the order in which to pass arguments and it's clearer what's going on without having to look at the original function signature.
mathesar/api/viewsets/tables.py
Outdated
@@ -105,18 +107,21 @@ def previews(self, request, pk=None): | |||
raise ValidationError("Incorrect number of columns in request.") | |||
|
|||
table_data = TableSerializer(table, context={"request": request}).data | |||
try: | |||
with exception_transformer({**dict.fromkeys([DataError, IntegrityError], ExceptionTransformerDetail(400, | |||
4001, |
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.
How do we generate numbers for error codes and make sure they are unique?
I think it might make sense to have a mapping of exceptions and error codes somewhere central and use it here.
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 will replace the error codes with Enum
preview_records = table.get_preview(columns) | ||
except (DataError, IntegrityError) as e: | ||
if type(e.orig) == InvalidTextRepresentation or type(e.orig) == CheckViolation: |
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.
It seems like we've lost this additional if
check in the refactor. I don't think all DataError
and IntegrityError
instances are indicative of an invalid type cast.
If we use are using a global DRF error handler, we might have to raise a custom APIException in order to assign a proper error code. I am on the fence tbh, I prefer to use drf error handler and lots of custom |
I think it's fine to have to raise custom |
Replace context manager error handling with custom ApiExceptions
…, to help convert validation errors from serializer to proper error body Add custom exception handler to convert exceptions that don't conform the error spec
…kage Fix error handler to look for default error handlers using the class of the exception raised
@kgodey I have replaced context managers with drf error handlers, let me know if your thoughts on this. I find this to be better as it seems to fit the codebase. @dmos62 @mathemancer I would love to hear your opinions too if you have any |
I went down the rabbit hole trying to replicate golang syle error handling using context managers as it looked more dry, but it was not verbose and made the code less explicit to the readers. @dmos62 Based on your comment I made the error codes centralised with option for the caller to supply their own if necessary |
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.
@silentninja Added some feedback.
mathesar/exceptions/exceptions.py
Outdated
|
||
ExceptionBody = namedtuple('ExceptionBody', | ||
[ | ||
'code', |
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.
Per the spec, this should be error_code
.
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.
Made an intentional change as drf
seems to be using code
rather than error_code
. Moreover code
seems apt as it is already inside an error object
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.
Okay, that's fine with me, but please update the spec too.
mathesar/exceptions/exceptions.py
Outdated
|
||
from mathesar.exceptions.error_codes import ErrorCodes | ||
|
||
ExceptionBody = namedtuple('ExceptionBody', |
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 think this will be most readable in one of these two options:
ExceptionBody = namedtuple('ExceptionBody', ['code', 'message', 'field', 'details'], defaults=[None, None])
or
ExceptionBody = namedtuple(
'ExceptionBody',
['code', 'message', 'field', 'details'],
defaults=[None, None]
)
mathesar/api/viewsets/tables.py
Outdated
|
||
column_names = [col["name"] for col in columns] | ||
if not len(column_names) == len(set(column_names)): | ||
raise ValidationError("Column names must be distinct") | ||
raise CustomValidationError([ExceptionBody(ErrorCodes.DistinctColumnNameRequired.value, |
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.
This is more involved than I think is necessary. It should be possible to just say something like:
raise DistinctColumnsRequiredException(message="Column names must be distinct")
The error code should be associated with the DistinctColumnsRequiredException
in the exceptions
module, it shouldn't need to be defined here.
This validation logic should ideally be in the validate_columns
method of the serializer, not here (see docs). Then it will automatically associate it with the columns
field and we don't have to do it manually.
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.
The error code should be associated with the DistinctColumnsRequiredException in the exceptions module, it shouldn't need to be defined here.
Yes, that is the idea, I haven't added other custom Exceptions yet as it will be dealt with in this issue. But do you think it will be better to create a new Exception for one-off validations like this? I was looking at using the message
or error code
to match with the relevant information, similar to the drf friendly errors library but found there are places where a different message is needed for a similar error code.
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.
This validation logic should ideally be in the validate_columns method of the serializer, not here
I am aware of it, been itching to move it for a long it, but didn't want to pollute the PR, but it does seem like a good time to move them into serializers
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.
Do you think it will be better to create a new Exception for one-off validations like this?
I think it will be easiest to have a single pattern to follow for tying error codes to exceptions, even for one-off exceptions.
It's fine to pass different message
values to the same exception, you can do this for Python exceptions as well so it's a common pattern.
it does seem like a good time to move them into serializers
Since you're already editing that particular code, I think it's fine to do it now. You don't have to do it for all of the code in this PR.
mathesar/exceptions/error_codes.py
Outdated
@@ -0,0 +1,13 @@ | |||
from enum import Enum, unique |
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 think this should all live within mathessar/api
, since these are only API related exceptions.
…onse which does not conform to the api spec to proper response
Together with Kriti's comments, this makes sense to me. |
Add MathesarErrorMixin to serializers in order to change the validation errors into correct error format that conforms to the spec
…spective Modelserializer Add MultipleDataFiles Custom exception class and its respective error code Add Duplicate table exception class and its respective error code
@silentninja I don't have any major suggestions to add; I'm just glad you're taking up this heroic adventure. |
…own within a serializer Rename CustomValidationError to GenericValidationError Move table preview validation to serializers
…nsupportedTypeException
… used in RecordViewset Add Api error classes for InvalidTableError to be used in DataFileViewset Change status of Api exception for UnsupportedTypeException to 500 Server error by default
Raise exception if error response is not formatted correctly in debug mode
…into mathesar-623-error-consistency
Add error code for `InvalidTableError` Add `ListSerializer` field error codes
Fix Exception handler to raise Exception if a non Api error is made during debug mode
Fix PolyMorphicSerializer to change class whenever mapping happens Fix exception handler to add missing error codes Fix test cases to reflect new error structure
Replaced by PR #1016 |
Fixes #623
This PR introduces custom
Api Exception
and customError Handler
to handle nonApiException
.ApiException
subclass which contains theerror code
, reusable logic for parsing the exception. It is responsible for converting the exception into a proper exception body according to the spec.ApiException
, if an equivalentApiException
does not exist, it converts it to aUnclassifiedException
(error code: 49XX), with the message as the string representation of the exception that is thrown.Checklist
Update index.md
).master
branch of the repositoryvisible errors.
Developer Certificate of Origin
Developer Certificate of Origin