Drop ImmediateResponse

This commit is contained in:
Tom Christie 2012-08-26 23:06:52 +01:00
parent edd8f5963c
commit 73cc77553e
6 changed files with 56 additions and 53 deletions

View File

@ -1,9 +1,15 @@
"""
Handled exceptions raised by REST framework.
In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from djangorestframework import status from djangorestframework import status
class ParseError(Exception): class ParseError(Exception):
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request' default_detail = 'Malformed request.'
def __init__(self, detail=None): def __init__(self, detail=None):
self.detail = detail or self.default_detail self.detail = detail or self.default_detail
@ -11,7 +17,7 @@ class ParseError(Exception):
class PermissionDenied(Exception): class PermissionDenied(Exception):
status_code = status.HTTP_403_FORBIDDEN status_code = status.HTTP_403_FORBIDDEN
default_detail = 'You do not have permission to access this resource' default_detail = 'You do not have permission to access this resource.'
def __init__(self, detail=None): def __init__(self, detail=None):
self.detail = detail or self.default_detail self.detail = detail or self.default_detail
@ -19,19 +25,30 @@ class PermissionDenied(Exception):
class MethodNotAllowed(Exception): class MethodNotAllowed(Exception):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = "Method '%s' not allowed" default_detail = "Method '%s' not allowed."
def __init__(self, method, detail): def __init__(self, method, detail=None):
self.detail = (detail or self.default_detail) % method self.detail = (detail or self.default_detail) % method
class UnsupportedMediaType(Exception): class UnsupportedMediaType(Exception):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
default_detail = "Unsupported media type '%s' in request" default_detail = "Unsupported media type '%s' in request."
def __init__(self, media_type, detail=None): def __init__(self, media_type, detail=None):
self.detail = (detail or self.default_detail) % media_type self.detail = (detail or self.default_detail) % media_type
# class Throttled(Exception):
# def __init__(self, detail): class Throttled(Exception):
# self.detail = detail status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_detail = "Request was throttled. Expected available in %d seconds."
def __init__(self, wait, detail=None):
import math
self.detail = (detail or self.default_detail) % int(math.ceil(wait))
REST_FRAMEWORK_EXCEPTIONS = (
ParseError, PermissionDenied, MethodNotAllowed,
UnsupportedMediaType, Throttled
)

View File

@ -6,9 +6,7 @@ Permission behavior is provided by mixing the :class:`mixins.PermissionsMixin` c
""" """
from django.core.cache import cache from django.core.cache import cache
from djangorestframework import status from djangorestframework.exceptions import PermissionDenied, Throttled
from djangorestframework.exceptions import PermissionDenied
from djangorestframework.response import ImmediateResponse
import time import time
__all__ = ( __all__ = (
@ -24,11 +22,6 @@ __all__ = (
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
_503_SERVICE_UNAVAILABLE = ImmediateResponse(
{'detail': 'request was throttled'},
status=status.HTTP_503_SERVICE_UNAVAILABLE)
class BasePermission(object): class BasePermission(object):
""" """
A base class from which all permission classes should inherit. A base class from which all permission classes should inherit.
@ -192,7 +185,7 @@ class BaseThrottle(BasePermission):
""" """
self.history.insert(0, self.now) self.history.insert(0, self.now)
cache.set(self.key, self.history, self.duration) cache.set(self.key, self.history, self.duration)
header = 'status=SUCCESS; next=%s sec' % self.next() header = 'status=SUCCESS; next=%.2f sec' % self.next()
self.view.headers['X-Throttle'] = header self.view.headers['X-Throttle'] = header
def throttle_failure(self): def throttle_failure(self):
@ -200,9 +193,10 @@ class BaseThrottle(BasePermission):
Called when a request to the API has failed due to throttling. Called when a request to the API has failed due to throttling.
Raises a '503 service unavailable' response. Raises a '503 service unavailable' response.
""" """
header = 'status=FAILURE; next=%s sec' % self.next() wait = self.next()
header = 'status=FAILURE; next=%.2f sec' % wait
self.view.headers['X-Throttle'] = header self.view.headers['X-Throttle'] = header
raise _503_SERVICE_UNAVAILABLE raise Throttled(wait)
def next(self): def next(self):
""" """
@ -215,7 +209,7 @@ class BaseThrottle(BasePermission):
available_requests = self.num_requests - len(self.history) + 1 available_requests = self.num_requests - len(self.history) + 1
return '%.2f' % (remaining_duration / float(available_requests)) return remaining_duration / float(available_requests)
class PerUserThrottling(BaseThrottle): class PerUserThrottling(BaseThrottle):

View File

@ -161,25 +161,3 @@ class Response(SimpleTemplateResponse):
}, },
status=status.HTTP_406_NOT_ACCEPTABLE, status=status.HTTP_406_NOT_ACCEPTABLE,
view=self.view, request=self.request, renderers=[renderer]) view=self.view, request=self.request, renderers=[renderer])
class ImmediateResponse(Response, Exception):
"""
An exception representing an Response that should be returned immediately.
Any content should be serialized as-is, without being filtered.
"""
#TODO: this is just a temporary fix, the whole rendering/support for ImmediateResponse, should be remade : see issue #163
def render(self):
try:
return super(Response, self).render()
except ImmediateResponse:
renderer, media_type = self._determine_renderer()
self.renderers.remove(renderer)
if len(self.renderers) == 0:
raise RuntimeError('Caught an ImmediateResponse while '\
'trying to render an ImmediateResponse')
return self.render()
def __init__(self, *args, **kwargs):
self.response = Response(*args, **kwargs)

View File

@ -4,7 +4,7 @@ import unittest
from django.conf.urls.defaults import patterns, url, include from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase from django.test import TestCase
from djangorestframework.response import Response, NotAcceptable, ImmediateResponse from djangorestframework.response import Response, NotAcceptable
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework import status from djangorestframework import status

View File

@ -45,7 +45,7 @@ class ThrottlingTests(TestCase):
request = self.factory.get('/') request = self.factory.get('/')
for dummy in range(4): for dummy in range(4):
response = MockView.as_view()(request) response = MockView.as_view()(request)
self.assertEqual(503, response.status_code) self.assertEqual(429, response.status_code)
def set_throttle_timer(self, view, value): def set_throttle_timer(self, view, value):
""" """
@ -62,7 +62,7 @@ class ThrottlingTests(TestCase):
request = self.factory.get('/') request = self.factory.get('/')
for dummy in range(4): for dummy in range(4):
response = MockView.as_view()(request) response = MockView.as_view()(request)
self.assertEqual(503, response.status_code) self.assertEqual(429, response.status_code)
# Advance the timer by one second # Advance the timer by one second
self.set_throttle_timer(MockView, 1) self.set_throttle_timer(MockView, 1)
@ -90,7 +90,7 @@ class ThrottlingTests(TestCase):
""" """
Ensure request rate is limited globally per View for PerViewThrottles Ensure request rate is limited globally per View for PerViewThrottles
""" """
self.ensure_is_throttled(MockView_PerViewThrottling, 503) self.ensure_is_throttled(MockView_PerViewThrottling, 429)
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers): def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
""" """

View File

@ -6,12 +6,14 @@ By setting or modifying class attributes on your view, you change it's predefine
""" """
import re import re
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View as DjangoView, apply_markdown from djangorestframework.compat import View as DjangoView, apply_markdown
from djangorestframework.response import Response, ImmediateResponse from djangorestframework.response import Response
from djangorestframework.request import Request from djangorestframework.request import Request
from djangorestframework import renderers, parsers, authentication, permissions, status, exceptions from djangorestframework import renderers, parsers, authentication, permissions, status, exceptions
@ -219,13 +221,27 @@ class View(DjangoView):
response[key] = value response[key] = value
return response return response
def handle_exception(self, exc):
"""
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
if isinstance(exc, exceptions.REST_FRAMEWORK_EXCEPTIONS):
return Response({'detail': exc.detail}, status=exc.status_code)
elif isinstance(exc, Http404):
return Response({'detail': 'Not found'},
status=status.HTTP_404_NOT_FOUND)
elif isinstance(exc, PermissionDenied):
return Response({'detail': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN)
raise
# Note: session based authentication is explicitly CSRF validated, # Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt. # all other authentication is CSRF exempt.
@csrf_exempt @csrf_exempt
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
request = Request(request, parsers=self.parsers, authentication=self.authentication) request = Request(request, parsers=self.parsers, authentication=self.authentication)
self.request = request self.request = request
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.headers = self.default_response_headers self.headers = self.default_response_headers
@ -244,10 +260,8 @@ class View(DjangoView):
response = handler(request, *args, **kwargs) response = handler(request, *args, **kwargs)
except ImmediateResponse, exc: except Exception as exc:
response = exc.response response = self.handle_exception(exc)
except (exceptions.ParseError, exceptions.PermissionDenied) as exc:
response = Response({'detail': exc.detail}, status=exc.status_code)
self.response = self.final(request, response, *args, **kwargs) self.response = self.final(request, response, *args, **kwargs)
return self.response return self.response
@ -265,4 +279,4 @@ class View(DjangoView):
for name, field in form.fields.iteritems(): for name, field in form.fields.iteritems():
field_name_types[name] = field.__class__.__name__ field_name_types[name] = field.__class__.__name__
content['fields'] = field_name_types content['fields'] = field_name_types
raise ImmediateResponse(content, status=status.HTTP_200_OK) raise Response(content, status=status.HTTP_200_OK)