GithubHelp home page GithubHelp logo

posthog / drf-exceptions-hog Goto Github PK

View Code? Open in Web Editor NEW
66.0 6.0 15.0 76 KB

Standardized and easy-to-parse API error responses for Django REST Framework (DRF).

License: MIT License

Python 100.00%

drf-exceptions-hog's Introduction

Logo

DRF Exceptions Hog

PyPI version License: MIT Code style: black

Standardized and easy-to-parse API error responses for Django REST Framework.

After reusing similar code in multiple projects, we realized this might actually help others. The problem we're trying to solve is that DRF exceptions tend to vary in format and therefore require complex parsing logic on the frontend, which generally needs to be implemented in more than one language or framework. This simple package standardizes the exception responses to simplify the parsing logic on the frontend and enable developers to provide clear errors to their users (instead of the cryptic or even shady-looking parsing errors). This package is inspired on the way Stripe API handles errors. See an example below.

You will get predictable responses like these:

// Example 1
{
  "type": "validation_error",
  "code": "required",
  "detail": "This field is required.",
  "attr": "name"
}

// Example 2
{
    "type": "authentication_error",
    "code": "permission_denied",
    "detail": "You do not have permission to perform this operation.",
    "attr": null
}

instead of these:

// Example 1
{
  "name": ["This field is required."]
}

// Example 2
{
    "detail": "You do not have permission to perform this operation."
}

Note: Currently we only support JSON responses. If you'd like us to support a different response format, please open an issue or a PR (see Contributing)

๐Ÿ”Œ Usage

To start using DRF Exceptions Hog please follow these instructions:

Install the package with pip

pip install drf-exceptions-hog

Update your DRF configuration on your settings.py.

REST_FRAMEWORK={
    "EXCEPTION_HANDLER": "exceptions_hog.exception_handler",
}

Optionally set additional configuration for the package.

EXCEPTIONS_HOG = {
    "EXCEPTION_REPORTING": "exceptions_hog.handler.exception_reporter",
    "ENABLE_IN_DEBUG": False,
    "NESTED_KEY_SEPARATOR": "__",
    "SUPPORT_MULTIPLE_EXCEPTIONS": False,
}
  • EXCEPTION_REPORTING: specify a method to call after an exception occurs. Particularly useful to report errors (e.g. through Sentry, NewRelic, ...). Default: exceptions_hog.handler.exception_reporter
  • ENABLE_IN_DEBUG: whether exceptions-hog should run when DEBUG = 1. It's useful to turn this off in debugging to get full error stack traces when developing. Defaut: False.
  • NESTED_KEY_SEPARATOR: customize the separator used for obtaining the attr name if the exception comes from nested objects (e.g. nested serializers). Default: __.
  • SUPPORT_MULTIPLE_EXCEPTIONS: whether exceptions-hog should return all exceptions in an error response. Useful particularly in form and serializer validation where multiple input exceptions can occur.

๐Ÿ“‘ Documentation

We're working on more comprehensive documentation. Feel free to open a PR to contribute to this. In the meantime, you will find the most relevant information for this package here.

Response structure

All responses handled by DRF Exceptions Hog have the following format:

{
  "type": "server_error",
  "code": "server_error",
  "detail": "Something went wrong.",
  "attr": null,
  "list": null
}

where:

  • type entails the high-level grouping of the type error returned (See Error Types).
  • code is a machine-friendly error code specific for this type of error (e.g. permission_denied, method_not_allowed, required)
  • detail will contain human-friendly information on the error (e.g. "This field is required.", "Authentication credentials were not provided.").
    • For security reasons (mainly to avoid leaking sensitive information) this attribute will return a generic error message for unhandled server exceptions, like an ImportError.
    • If you use Django localization, all our exception detail messages support using multiple languages.
  • attr will contain the name of the attribute to which the exception is related. Relevant mostly for validation_errors.
  • list will only be returned when multiple exceptions are enabled and the exception contains multiple exceptions (i.e. type = multiple).
  • extra is an extra attribute you can set on an exception to pass through extra content, normally in dict form.

Multiple exceptions

There are some cases when handling multiple exceptions in a single response can be helpful. For instance, if you have a form with multiple fields, each field can have their own validations, and a user could benefit from knowing everything that is wrong in a single pass. You can enable multiple exception support by setting the SUPPORT_MULTIPLE_EXCEPTIONS setting to True. When it's enabled, if multiple exceptions are raised (e.g. by a serializer), you will receive a response like this:

{
  "type": "multiple",
  "code": "multiple",
  "detail": "Multiple exceptions ocurred. Please check list for details.",
  "attr": null,
  "list": [
    {
      "type": "validation_error",
      "code": "required",
      "detail": "This field is required.",
      "attr": "email"
    },
    {
      "type": "validation_error",
      "code": "unsafe_password",
      "detail": "This password is unsafe.",
      "attr": "password"
    }
  ]
}

Error types

Our package introduces the following general error types (but feel free to add custom ones):

  • authentication_error indicates there is an authentication-related problem with the request (e.g. no authentication credentials provided, invalid or expired credentials provided, credentials have insufficient privileges, etc.)
  • invalid_request indicates a general issue with the request that must be fixed by the client, excluding validation errors (e.g. request has an invalid media type format, request is malformed, etc.)
  • multiple indicates multiple exceptions ocurred (only if enabled). See multiple exceptions for details.
  • server_error indicates a generic internal server error that needs to be addressed on the server.
  • throttled_error indicates the request is throttled or rate limited and must be retried by the client at a later time.
  • validation_error indicates the request has not passed validation and must be fixed by the client (e.g. a required attribute was not provided, an incorrect data type was passed for an attribute, etc.)

๐Ÿค Contributing

Want to help move this project forward? Read our CONTRIBUTING.md.

๐Ÿ‘ฉโ€๐Ÿ’ป Development

To run a local copy of the package for development, please follow these instructions:

  1. Clone the repository.

  2. [Optional]. Install and activate a virtual enviroment. Example:

    python3 -m venv env && source env/bin/activate
  3. Install the project dependencies and the test dependencies.

    python setup.py develop
    pip install -r requirements-test.txt
  4. Run the tests to make sure everything is working as expected.

    python runtests.py
  5. Start coding!

๐Ÿงฑ Requirements

  • This package requires at least Python 3.7 & Django 3.1
  • Supported Python versions: 3.7.x, 3.8.x & 3.9.x
  • Supported Django versions: 3.1.x & 3.2.x

๐Ÿ‘จโ€โš–๏ธ License

We โ™ฅ Open Source! This repository is MIT licensed by PostHog. Full license here.

drf-exceptions-hog's People

Contributors

abtinmo avatar joethreepwood avatar paolodamico avatar pauldambra avatar posthog-bot avatar timgl avatar twixes avatar zombie123456 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

drf-exceptions-hog's Issues

Force Multiple Exceptions Format

It would be useful to have only one error response format regardless of the number of errors as documenting and consuming both is a bit tedious.

So instead of:

if api_settings.SUPPORT_MULTIPLE_EXCEPTIONS and len(exception_list) > 1:
    create multiple format

Maybe something like?

if api_settings.FORCE_MULTIPLE_EXCEPTIONS or api_settings.SUPPORT_MULTIPLE_EXCEPTIONS and len(exception_list) > 1
    create multiple format

I can make a PR if you're interested.

Handle deeply nested validation errors

Say we have the following ValidationError with SUPPORT_MULTIPLE_EXCEPTIONS = True

{
        "parent_1": {
            "l1_attr_3": [
                [
                    ErrorDetail(
                        string="This field may not be blank.", code="blank"
                    ),
                ]
            ],
        },
        "parent_2": [
            [
                ErrorDetail(string="This field may not be blank.", code="blank"),
            ]
        ],
        "parent_3": [
            {
                "l1_attr_1": [
                    ErrorDetail(
                        string="This field may not be blank.", code="blank"
                    ),
                ],
                "l1_attr_2": [
                    ErrorDetail(
                        string="This field may not be blank.", code="blank"
                    ),
                ],
            },
            {
                "l1_attr_1": [
                    ErrorDetail(
                        string="This field may not be blank.", code="blank"
                    ),
                ],
            },
        ],
    }

The expected result is

{
    "type": "multiple",
    "code": "multiple",
    "detail": "Multiple exceptions occurred. Please check list for details.",
    "attr": None,
    "list": [
        {
            "type": "validation_error",
            "code": "blank",
            "detail": "This field may not be blank.",
            "attr": "parent_1__l1_attr_3__0",
        },
        {
            "type": "validation_error",
            "code": "blank",
            "detail": "This field may not be blank.",
            "attr": "parent_2__0",
        },
        {
            "type": "validation_error",
            "code": "blank",
            "detail": "This field may not be blank.",
            "attr": "parent_3__0__l1_attr_1",
        },
        {
            "type": "validation_error",
            "code": "blank",
            "detail": "This field may not be blank.",
            "attr": "parent_3__0__l1_attr_2",
        },
        {
            "type": "validation_error",
            "code": "blank",
            "detail": "This field may not be blank.",
            "attr": "parent_3__1__l1_attr_1",
        },
    ],
}

Instead we get this

{
    "type": "multiple",
    "code": "multiple",
    "detail": "Multiple exceptions ocurred. Please check list for details.",
    "attr": None,
    "list": [
        {
            "type": "validation_error",
            "code": "['blank']",
            "detail": "[ErrorDetail(string='This field may not be blank.', code='blank')]",
            "attr": "parent_1__l1_attr_3",
        },
        {
            "type": "validation_error",
            "code": "['blank']",
            "detail": "[ErrorDetail(string='This field may not be blank.', code='blank')]",
            "attr": "parent_2",
        },
        {
            "type": "validation_error",
            "code": "{'l1_attr_1': ['blank'], 'l1_attr_2': ['blank']}",
            "detail": "{'l1_attr_1': [ErrorDetail(string='This field may not be blank.', code='blank')], 'l1_attr_2': [ErrorDetail(string='This field may not be blank.', code='blank')]}",
            "attr": "parent_3",
        },
    ],
}

Not handling Validation error for nested serializers

Hi, thanks for this repo.

I got one issue when working with a nested serializer.

Let's say I have a UserSerializer with nested foreign key extra_info

class UserSerializer(serializers.Serializer):
    class UserExtraInfoSerializer(serializers.Serializer):
        mobile_phone_number = serializers.CharField(max_length=15, required=True)

    first_name = serializers.CharField(max_length=100)
    extra_info = UserExtraInfoSerializer()

Then, let's assume client wants to send a request with the below POST data to the view handled by UserSerializer:

{"first_name": "Someone", "extra_info": {}}

The client should receive ValidationError, because when extra_info is present mobile_phone_number is required. But because it is not handled in drf-exceptions-hog. I guess I found the place where the fix is necessary:

key = next(iter(codes)) # Get first key
code = codes[key] if isinstance(codes[key], str) else codes[key][0]

Because we are handling nested serializer, we need to have one more condition to check if there is dict inside (the best even iterate over all dictionaries to find the root). I will find a workaround for my project, maybe even make PR, hopefully

TypeError: sequence item 1: expected str instance, int found

Sentry Issue: POSTHOG-8NQ

ValidationError: {'byweekday': {0: [ErrorDetail(string='"0" is not a valid choice.', code='invalid_choice')], 1: [ErrorDetail(string='"1" is not a valid choice.', code='invalid_choice')], 2: [ErrorDetail(string='"2" is not a valid choice.', code='invalid_choice')], 3: [ErrorDetail(string='"3" is not a valid choice.', code='invalid_choice')], 4: [ErrorDetail(string='"4" is not a valid choice.', code='invalid_choice')], 5: [ErrorDetail(string='"5" is not a valid choice.', code='invalid_choice')], 6: [ErrorDetail(string='"6" is not a valid choice.', code='invalid_choice')]}}
  File "rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "rest_framework/mixins.py", line 18, in create
    serializer.is_valid(raise_exception=True)
  File "rest_framework/serializers.py", line 228, in is_valid
    raise ValidationError(self.errors)

TypeError: sequence item 1: expected str instance, int found
(3 additional frame(s) were not displayed)
...
  File "rest_framework/viewsets.py", line 125, in view
    return self.dispatch(request, *args, **kwargs)
  File "rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "rest_framework/views.py", line 466, in handle_exception
    response = exception_handler(exc, context)
  File "exceptions_hog/handler.py", line 299, in exception_handler
    attr=_get_attr(exception_list[0][1]),
  File "exceptions_hog/handler.py", line 201, in _get_attr
    return override_or_return(api_settings.NESTED_KEY_SEPARATOR.join(exception_key))

incorect behavior when validating a list

when DRF throws ValidationError for elements inside of a list, package populates code and detail fields with not usable data.

example:

models:

class Day(models.Model):
    name = models.CharField(max_length=50)


class Time(models.Model):
    day = models.ForeignKey(Day, on_delete=models.CASCADE, related_name="times")
    time = models.TimeField()

serializers

class TimeSerializer(serializers.ModelSerializer):
    day = serializers.IntegerField(required=False)

    class Meta:
        fields = "__all__"
        model = Time


class DaySerializer(serializers.ModelSerializer):
    times = TimeSerializer(many=True)

    class Meta:
        fields = "__all__"
        model = Day

    def create(self, validated_data):
        # handle nested creation
        # . . .

request body:


{
    "times": [{}],
    "name": ""
}

response:

{
    "type": "validation_error",
    "code": "{'time': ['required']}",
    "detail": "{'time': [ErrorDetail(string='This field is required.', code='required')]}",
    "attr": "times"
}

list response

{
    "type": "multiple",
    "code": "multiple",
    "detail": "Multiple exceptions ocurred. Please check list for details.",
    "attr": null,
    "list": [
        {
            "type": "validation_error",
            "code": "{'time': ['required']}",
            "detail": "{'time': [ErrorDetail(string='This field is required.', code='required')]}",
            "attr": "times"
        },
        {
            "type": "validation_error",
            "code": "blank",
            "detail": "This field may not be blank.",
            "attr": "name"
        }
    ]

How can we have all errors in a single response?

Hello, I'm trying to figure out if this is included (or if planned for later on) where we get all errors in a single JSON response.
For example, if I were to send a bad POST request with two blank fields, I'd only get an error for one of them:

{
    "type": "validation_error",
    "code": "blank",
    "detail": "This field may not be blank.",
    "attr": "FIELD_ONE"
}

How can we get something like:

errors: [
    {
        "type": "validation_error",
        "code": "blank",
        "detail": "This field may not be blank.",
        "attr": "FIELD_ONE"
    },
    {
        "type": "validation_error",
        "code": "blank",
        "detail": "This field may not be blank.",
        "attr": "FIELD_TWO"
    }
]

It really helps with sign up forms for example, since the user gets to see all failing fields the first time, instead of fixing them one by one every failing request.

Interaction with exceptions tracker

We need to make sure the package plays nice with exceptions tracker (like Sentry) and that it will still propagate the exceptions to be captured as expected.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.