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
class ParseError(Exception):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request'
default_detail = 'Malformed request.'
def __init__(self, detail=None):
self.detail = detail or self.default_detail
@ -11,7 +17,7 @@ class ParseError(Exception):
class PermissionDenied(Exception):
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):
self.detail = detail or self.default_detail
@ -19,19 +25,30 @@ class PermissionDenied(Exception):
class MethodNotAllowed(Exception):
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
class UnsupportedMediaType(Exception):
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):
self.detail = (detail or self.default_detail) % media_type
# class Throttled(Exception):
# def __init__(self, detail):
# self.detail = detail
class Throttled(Exception):
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 djangorestframework import status
from djangorestframework.exceptions import PermissionDenied
from djangorestframework.response import ImmediateResponse
from djangorestframework.exceptions import PermissionDenied, Throttled
import time
__all__ = (
@ -24,11 +22,6 @@ __all__ = (
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
_503_SERVICE_UNAVAILABLE = ImmediateResponse(
{'detail': 'request was throttled'},
status=status.HTTP_503_SERVICE_UNAVAILABLE)
class BasePermission(object):
"""
A base class from which all permission classes should inherit.
@ -192,7 +185,7 @@ class BaseThrottle(BasePermission):
"""
self.history.insert(0, self.now)
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
def throttle_failure(self):
@ -200,9 +193,10 @@ class BaseThrottle(BasePermission):
Called when a request to the API has failed due to throttling.
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
raise _503_SERVICE_UNAVAILABLE
raise Throttled(wait)
def next(self):
"""
@ -215,7 +209,7 @@ class BaseThrottle(BasePermission):
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):

View File

@ -161,25 +161,3 @@ class Response(SimpleTemplateResponse):
},
status=status.HTTP_406_NOT_ACCEPTABLE,
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.test import TestCase
from djangorestframework.response import Response, NotAcceptable, ImmediateResponse
from djangorestframework.response import Response, NotAcceptable
from djangorestframework.views import View
from djangorestframework.compat import RequestFactory
from djangorestframework import status

View File

@ -45,7 +45,7 @@ class ThrottlingTests(TestCase):
request = self.factory.get('/')
for dummy in range(4):
response = MockView.as_view()(request)
self.assertEqual(503, response.status_code)
self.assertEqual(429, response.status_code)
def set_throttle_timer(self, view, value):
"""
@ -62,7 +62,7 @@ class ThrottlingTests(TestCase):
request = self.factory.get('/')
for dummy in range(4):
response = MockView.as_view()(request)
self.assertEqual(503, response.status_code)
self.assertEqual(429, response.status_code)
# Advance the timer by one second
self.set_throttle_timer(MockView, 1)
@ -90,7 +90,7 @@ class ThrottlingTests(TestCase):
"""
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):
"""

View File

@ -6,12 +6,14 @@ By setting or modifying class attributes on your view, you change it's predefine
"""
import re
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.views.decorators.csrf import csrf_exempt
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 import renderers, parsers, authentication, permissions, status, exceptions
@ -219,13 +221,27 @@ class View(DjangoView):
response[key] = value
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,
# all other authentication is CSRF exempt.
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
request = Request(request, parsers=self.parsers, authentication=self.authentication)
self.request = request
self.args = args
self.kwargs = kwargs
self.headers = self.default_response_headers
@ -244,10 +260,8 @@ class View(DjangoView):
response = handler(request, *args, **kwargs)
except ImmediateResponse, exc:
response = exc.response
except (exceptions.ParseError, exceptions.PermissionDenied) as exc:
response = Response({'detail': exc.detail}, status=exc.status_code)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.final(request, response, *args, **kwargs)
return self.response
@ -265,4 +279,4 @@ class View(DjangoView):
for name, field in form.fields.iteritems():
field_name_types[name] = field.__class__.__name__
content['fields'] = field_name_types
raise ImmediateResponse(content, status=status.HTTP_200_OK)
raise Response(content, status=status.HTTP_200_OK)