From e8b4fcef4ffc748576979e3d0dfcca77f020adb4 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 6 Jun 2012 11:07:51 -0400 Subject: [PATCH 1/3] Handle malformed HTTP Accept values Some clients cause tastypie to return a 500 error because they send a request with an HTTP Accept header which contains an invalid MIME type (e.g. any value without a "/" other than "*", which mimeparse handles). determine_format() now raises BadRequest any time mimeparse fails. Resources now handle this by returning HTTP 400 immediately rather than the normal error response --- tastypie/resources.py | 12 +++++++++--- tastypie/utils/mime.py | 11 ++++++++++- tests/basic/tests/http.py | 19 +++++++++++++++++++ tests/core/tests/utils.py | 5 +++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/tastypie/resources.py b/tastypie/resources.py index 7c9b8cc58..880832192 100644 --- a/tastypie/resources.py +++ b/tastypie/resources.py @@ -223,7 +223,7 @@ def wrapper(request, *args, **kwargs): return response except (BadRequest, fields.ApiFieldError), e: - data = {"error": e.args[0]} + data = {"error": e.args[0] if getattr(e, 'args') else ''} return self.error_response(request, data, response_class=http.HttpBadRequest) except ValidationError, e: data = {"error": e.messages} @@ -1190,13 +1190,19 @@ def error_response(self, request, errors, response_class=None): if response_class is None: response_class = http.HttpBadRequest + desired_format = None + if request: if request.GET.get('callback', None) is None: - desired_format = self.determine_format(request) + try: + desired_format = self.determine_format(request) + except BadRequest: + pass # Fall through to default handler below else: # JSONP can cause extra breakage. desired_format = 'application/json' - else: + + if not desired_format: desired_format = self._meta.default_format try: diff --git a/tastypie/utils/mime.py b/tastypie/utils/mime.py index c2f66da96..a0c66c818 100644 --- a/tastypie/utils/mime.py +++ b/tastypie/utils/mime.py @@ -1,5 +1,7 @@ import mimeparse +from tastypie.exceptions import BadRequest + def determine_format(request, serializer, default_format='application/json'): """ @@ -13,6 +15,9 @@ def determine_format(request, serializer, default_format='application/json'): If still no format is found, returns the ``default_format`` (which defaults to ``application/json`` if not provided). + + NOTE: callers *must* be prepared to handle BadRequest exceptions due to + malformed HTTP request headers! """ # First, check if they forced the format. if request.GET.get('format'): @@ -30,7 +35,11 @@ def determine_format(request, serializer, default_format='application/json'): # https://github.com/toastdriven/django-tastypie/issues#issue/12 for # more information. formats.reverse() - best_format = mimeparse.best_match(formats, request.META['HTTP_ACCEPT']) + + try: + best_format = mimeparse.best_match(formats, request.META['HTTP_ACCEPT']) + except ValueError: + raise BadRequest('Invalid Accept header') if best_format: return best_format diff --git a/tests/basic/tests/http.py b/tests/basic/tests/http.py index ebc524748..cc0a05a57 100644 --- a/tests/basic/tests/http.py +++ b/tests/basic/tests/http.py @@ -22,6 +22,25 @@ def test_get_apis_json(self): self.assertEqual(response.status, 200) self.assertEqual(data, '{"cached_users": {"list_endpoint": "/api/v1/cached_users/", "schema": "/api/v1/cached_users/schema/"}, "notes": {"list_endpoint": "/api/v1/notes/", "schema": "/api/v1/notes/schema/"}, "private_cached_users": {"list_endpoint": "/api/v1/private_cached_users/", "schema": "/api/v1/private_cached_users/schema/"}, "public_cached_users": {"list_endpoint": "/api/v1/public_cached_users/", "schema": "/api/v1/public_cached_users/schema/"}, "users": {"list_endpoint": "/api/v1/users/", "schema": "/api/v1/users/schema/"}}') + def test_get_apis_invalid_accept(self): + connection = self.get_connection() + connection.request('GET', '/api/v1/', headers={'Accept': 'invalid'}) + response = connection.getresponse() + connection.close() + data = response.read() + self.assertEqual(response.status, 400, "Invalid HTTP Accept headers should return HTTP 400") + + def test_get_resource_invalid_accept(self): + """Invalid HTTP Accept headers should return HTTP 400""" + # We need to test this twice as there's a separate dispatch path for resources: + + connection = self.get_connection() + connection.request('GET', '/api/v1/notes/', headers={'Accept': 'invalid'}) + response = connection.getresponse() + connection.close() + data = response.read() + self.assertEqual(response.status, 400, "Invalid HTTP Accept headers should return HTTP 400") + def test_get_apis_xml(self): connection = self.get_connection() connection.request('GET', '/api/v1/', headers={'Accept': 'application/xml'}) diff --git a/tests/core/tests/utils.py b/tests/core/tests/utils.py index 007cb372d..d53468900 100644 --- a/tests/core/tests/utils.py +++ b/tests/core/tests/utils.py @@ -1,5 +1,7 @@ from django.http import HttpRequest from django.test import TestCase + +from tastypie.exceptions import BadRequest from tastypie.serializers import Serializer from tastypie.utils.mime import determine_format, build_content_type @@ -84,3 +86,6 @@ def test_determine_format(self): request.META = {'HTTP_ACCEPT': 'text/javascript,application/json'} self.assertEqual(determine_format(request, serializer), 'application/json') + + request.META = {'HTTP_ACCEPT': 'bogon'} + self.assertRaises(BadRequest, determine_format, request, serializer) From e05ee4c9969d968c43c09324b7b826f819dda3eb Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Thu, 7 Jun 2012 11:00:05 -0400 Subject: [PATCH 2/3] wrap_view: handle BadRequest exceptions gracefully --- tastypie/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tastypie/api.py b/tastypie/api.py index 94bb6a50a..031b6cd1b 100644 --- a/tastypie/api.py +++ b/tastypie/api.py @@ -2,7 +2,7 @@ from django.conf.urls.defaults import * from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseBadRequest from tastypie.exceptions import NotRegistered, BadRequest from tastypie.serializers import Serializer from tastypie.utils import trailing_slash, is_valid_jsonp_callback_value @@ -72,7 +72,10 @@ def canonical_resource_for(self, resource_name): def wrap_view(self, view): def wrapper(request, *args, **kwargs): - return getattr(self, view)(request, *args, **kwargs) + try: + return getattr(self, view)(request, *args, **kwargs) + except BadRequest: + return HttpResponseBadRequest() return wrapper def override_urls(self): @@ -137,6 +140,7 @@ def top_level(self, request, api_name=None): } desired_format = determine_format(request, serializer) + options = {} if 'text/javascript' in desired_format: From a20c21ef1b648c0752ee92068dd9469108c650dd Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Thu, 14 Feb 2013 15:44:08 -0500 Subject: [PATCH 3/3] PEP-8: replace legacy has_key() with ``foo in bar`` --- tastypie/utils/mime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tastypie/utils/mime.py b/tastypie/utils/mime.py index a0c66c818..afb4ec1b1 100644 --- a/tastypie/utils/mime.py +++ b/tastypie/utils/mime.py @@ -25,7 +25,7 @@ def determine_format(request, serializer, default_format='application/json'): return serializer.get_mime_for_format(request.GET['format']) # If callback parameter is present, use JSONP. - if request.GET.has_key('callback'): + if 'callback' in request.GET: return serializer.get_mime_for_format('jsonp') # Try to fallback on the Accepts header.