aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2012-08-26 23:06:52 +0100
committerTom Christie2012-08-26 23:06:52 +0100
commit73cc77553ed5411f1959a51574b156a47ad5340d (patch)
tree0b3a0f707fefd116a274adcfa80364596e8df76d
parentedd8f5963cb32063931a1557d3c6ac29d19b3425 (diff)
downloaddjango-rest-framework-73cc77553ed5411f1959a51574b156a47ad5340d.tar.bz2
Drop ImmediateResponse
-rw-r--r--djangorestframework/exceptions.py33
-rw-r--r--djangorestframework/permissions.py18
-rw-r--r--djangorestframework/response.py22
-rw-r--r--djangorestframework/tests/response.py2
-rw-r--r--djangorestframework/tests/throttling.py6
-rw-r--r--djangorestframework/views.py28
6 files changed, 56 insertions, 53 deletions
diff --git a/djangorestframework/exceptions.py b/djangorestframework/exceptions.py
index 3f7e9029..315c1b1d 100644
--- a/djangorestframework/exceptions.py
+++ b/djangorestframework/exceptions.py
@@ -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
+)
diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py
index b56d8a32..bdda4def 100644
--- a/djangorestframework/permissions.py
+++ b/djangorestframework/permissions.py
@@ -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):
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index ea9a938c..ac16e79a 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -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)
diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py
index 07d0f4fb..ded0a3da 100644
--- a/djangorestframework/tests/response.py
+++ b/djangorestframework/tests/response.py
@@ -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
diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py
index d307cd32..ad22d2d2 100644
--- a/djangorestframework/tests/throttling.py
+++ b/djangorestframework/tests/throttling.py
@@ -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):
"""
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 36d05721..a7540e0c 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -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)