Custom throttling response in django rest framework
Asked Answered
H

3

13

I am using DRF for rest apis so now i am applying throttling to my apis. For that I created following throttle scopes

  1. userRateThrottle

  2. anonRateThrottle

  3. burstRateThrottle

  4. perViewsThrottles (varies with view)

currently i getting below response:

{"detail":"Request was throttled. Expected available in 32.0 seconds."}

I want response something like this:

{"message":"request limit exceeded","availableIn":"32.0 seconds","throttleType":"type"}

There is nothing in DRF docs for customisation. How can i customise my response according to requirement?

Hampden answered 4/10, 2015 at 10:12 Comment(0)
S
25

To do that, you can implement a custom exception handler function that returns the custom response in case of a Throttled exceptions.

from rest_framework.views import exception_handler
from rest_framework.exceptions import Throttled

def custom_exception_handler(exc, context):
    # Call REST framework's default exception handler first,
    # to get the standard error response.
    response = exception_handler(exc, context)

    if isinstance(exc, Throttled): # check that a Throttled exception is raised
        custom_response_data = { # prepare custom response data
            'message': 'request limit exceeded',
            'availableIn': '%d seconds'%exc.wait
        }
        response.data = custom_response_data # set the custom response data on response object

  return response

Then, you need to add this custom exception handler to your DRF settings.

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}

I think it would be slightly difficult to know the throttleType without changing some DRF code as DRF raises a Throttled exception in case of any the Throttle classes throttling a request. No information is passed to the Throttled exception about which throttle_class is raising that exception.

Samhita answered 4/10, 2015 at 18:53 Comment(0)
E
20

You can change message of throttled response by overriding throttled methods of your view. For example:

from rest_framework.exceptions import Throttled

class SomeView(APIView):
    def throttled(self, request, wait):
        raise Throttled(detail={
              "message":"request limit exceeded",
              "availableIn":f"{wait} seconds",
              "throttleType":"type"
        })
Etheleneethelin answered 27/2, 2019 at 10:3 Comment(1)
Wow! A very elegant solutionCazzie
F
5

I know this is an old thread, but adding to Rahul's answer, here's a way to include the throttleType in the message:

You will first need to override the Throttled exception class:

  1. Create a file called rest_exceptions.py, and create the following:

    import math
    import inspect
    from django.utils.encoding import force_text
    from django.utils.translation import ungettext
    from rest_framework import exceptions, throttling
    
    class CustomThrottled(exceptions.Throttled):
    
        def __init__(self, wait=None, detail=None, throttle_instance=None):
            if throttle_instance is None:
                self.throttle_instance = None
            else:
                self.throttle_instance = throttle_instance
    
            if detail is not None:
                self.detail = force_text(detail)
            else:
                self.detail = force_text(self.default_detail)
    
            if wait is None:
                self.wait = None
            else:
                self.wait = math.ceil(wait)
    

    Here you add a kwarg for the instance of the throttle that raises the exception (if provided). You can also override the behavior of the detail message, and do what you wish with the wait value as well. I've decided to not concatenate detail and wait, but rather use the raw detail message.

  2. Next, you'll want to create a custom viewset that passes the throttler to the throttled exception. Create a file called rest_viewsets.py and create the following:

    from rest_framework import viewsets
    from .rest_exceptions import CustomThrottled
    
    class ThrottledViewSet(viewsets.ViewSet):
        """
        Adds customizability to the throtted method for better clarity.
        """
    
        throttled_exception_class = CustomThrottled
    
        def throttled(self, request, wait, throttle_instance=None):
            """
            If request is throttled, determine what kind of exception to raise.
            """
            raise self.get_throttled_exception_class()(wait, detail=self.get_throttled_message(request),
                                                       throttle_instance=throttle_instance)
    
        def get_throttled_message(self, request):
            """
            Add a custom throttled exception message to pass to the user.
            Note that this does not account for the wait message, which will be added at the
            end of this message.
            """
            return None
    
        def get_throttled_exception_class(self):
            """
            Return the throttled exception class to use.
            """
            return self.throttled_exception_class
    
        def check_throttles(self, request):
                """
                Check if request should be throttled.
                Raises an appropriate exception if the request is throttled.
                """
                for throttle in self.get_throttles():
                    if not throttle.allow_request(request, self):
                        self.throttled(request, throttle.wait(), throttle_instance=throttle)
    
  3. Now that you have a custom exception that will store the throttle instance, and a viewset that will pass the instance to the exception, your next step is to implement a view that inherits this viewset, and also uses one of the throttle classes you had listed. In your views.py, under the intended view (since you didn't provide that, I'm going to call it MyViewset):

    from .rest_viewsets import ThrottledViewSet
    from rest_framework import throttling
    
    class MyViewset(ThrottledViewSet):
        throttle_classes = (throttling.userRateThrottle,)  # Add more here as you wish
        throttled_exception_class = CustomThrottled  # This is the default already, but let's be specific anyway
    
        def get_throttled_message(self, request):
            """Add a custom message to the throttled error."""
            return "request limit exceeded"
    
  4. At this point, your app will check for throttles like usual, but will also pass along the throttle instance as well. I have also overridden the throttle message to what you wanted. We can now tap into the solution that Rahul has provided, with a few modifications. Create a custom exception handler:

    from rest_framework.views import exception_handler
    from .rest_exceptions import CustomThrottled
    
    def custom_exception_handler(exc, context):
        # Call REST framework's default exception handler first,
        # to get the standard error response.
        response = exception_handler(exc, context)
    
        if isinstance(exc, CustomThrottled): # check that a CustomThrottled exception is raised
            custom_response_data = { # prepare custom response data
                'message': exc.detail,
                'availableIn': '%d seconds'%exc.wait,
                'throttleType': type(exc.throttle_instance).__name__
            }
            response.data = custom_response_data # set the custom response data on response object
    
      return response
    

    You could easily access any other attribute of the throttle class at this point, but you only wanted the class name.

  5. Last but not least, add your handler to the DRF settings:

    REST_FRAMEWORK = {
        'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
    }
    
Fineman answered 8/9, 2016 at 20:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.