mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-03 13:14:30 +03:00
Drop ImmediateResponse
This commit is contained in:
parent
edd8f5963c
commit
73cc77553e
|
@ -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
|
||||||
|
)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user