diff --git a/rest_framework/response.py b/rest_framework/response.py index 495423734..9986a421e 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -17,9 +17,16 @@ class Response(SimpleTemplateResponse): arbitrary media types. """ - def __init__(self, data=None, status=None, - template_name=None, headers=None, - exception=False, content_type=None): + def __init__( + self, + data=None, + status=None, + template_name=None, + headers=None, + exception=False, + content_type=None, + reason=None, + ): """ Alters the init arguments slightly. For example, drop 'template_name', and instead use 'data'. @@ -41,6 +48,8 @@ class Response(SimpleTemplateResponse): self.template_name = template_name self.exception = exception self.content_type = content_type + if reason: + self.reason_phrase = "{} ({})".format(self.status_text, reason) if headers: for name, value in headers.items(): diff --git a/rest_framework/views.py b/rest_framework/views.py index d1b5e4ed9..db19243fc 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -8,6 +8,7 @@ from django.http import Http404 from django.http.response import HttpResponseBase from django.utils.cache import cc_delim_re, patch_vary_headers from django.utils.encoding import smart_str +from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt from django.views.generic import View @@ -92,11 +93,15 @@ def exception_handler(exc, context): if isinstance(exc.detail, (list, dict)): data = exc.detail + reason = _('See response body for full details') else: - data = {'detail': exc.detail} + data = {"detail": exc.detail} + reason = str(exc.detail) + if isinstance(exc.detail, list) and len(exc.detail) == 1: + reason = exc.detail[0] set_rollback() - return Response(data, status=exc.status_code, headers=headers) + return Response(data, status=exc.status_code, headers=headers, reason=reason) return None diff --git a/tests/test_response.py b/tests/test_response.py index 0d5528dc9..4bf7c0abb 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -32,6 +32,7 @@ class MockTextMediaRenderer(BaseRenderer): DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' +DUMMYREASON = 'dummyreason' def RENDERER_A_SERIALIZER(x): @@ -78,6 +79,12 @@ class MockViewSettingContentType(APIView): return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview') +class MockViewSettingReason(APIView): + + def get(self, request, **kwargs): + return Response(DUMMYCONTENT, status=DUMMYSTATUS, reason=DUMMYREASON) + + class JSONView(APIView): parser_classes = (JSONParser,) @@ -125,7 +132,8 @@ urlpatterns = [ path('html1', HTMLView1.as_view()), path('html_new_model', HTMLNewModelView.as_view()), path('html_new_model_viewset', include(new_model_viewset_router.urls)), - path('restframework', include('rest_framework.urls', namespace='rest_framework')) + path('restframework', include('rest_framework.urls', namespace='rest_framework')), + path('with_reason', MockViewSettingReason.as_view()) ] @@ -283,3 +291,14 @@ class Issue807Tests(TestCase): self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') # self.assertContains(resp, 'Text comes here') # self.assertContains(resp, 'Text description.') + + +@override_settings(ROOT_URLCONF='tests.test_response') +class ReasonPhraseTests(TestCase): + def test_reason_is_set(self): + resp = self.client.get('/with_reason') + self.assertEqual("OK ({})".format(DUMMYREASON), resp.reason_phrase) + + def test_reason_phrase_with_no_reason(self): + resp = self.client.get('/') + self.assertEqual("OK", resp.reason_phrase) diff --git a/tests/test_views.py b/tests/test_views.py index 2648c9fb3..e1e035135 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,9 +1,11 @@ import copy +import re from django.test import TestCase from rest_framework import status from rest_framework.decorators import api_view +from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.settings import APISettings, api_settings from rest_framework.test import APIRequestFactory @@ -12,6 +14,9 @@ from rest_framework.views import APIView factory = APIRequestFactory() JSON_ERROR = 'JSON parse error - Expecting value:' +STATUS_CODE_400 = 'Bad Request' +REASON_PHRASE_RE = r"{} \({}.*\)".format(STATUS_CODE_400, JSON_ERROR) +VALIDATION_ERROR = "Data isn't valid!" class BasicView(APIView): @@ -39,6 +44,11 @@ class ErrorView(APIView): raise Exception +class ValidationErrorView(APIView): + def get(self, request, *args, **kwargs): + raise ValidationError(VALIDATION_ERROR) + + def custom_handler(exc, context): if isinstance(exc, SyntaxError): return Response({'error': 'SyntaxError'}, status=400) @@ -57,6 +67,11 @@ def error_view(request): raise Exception +@api_view(['GET']) +def validation_error_view(request): + raise ValidationError(VALIDATION_ERROR) + + def sanitise_json_error(error_dict): """ Exact contents of JSON error messages depend on the installed version @@ -69,32 +84,44 @@ def sanitise_json_error(error_dict): class ClassBasedViewIntegrationTests(TestCase): - def setUp(self): - self.view = BasicView.as_view() def test_400_parse_error(self): request = factory.post('/', 'f00bar', content_type='application/json') - response = self.view(request) + response = BasicView.as_view()(request) expected = { 'detail': JSON_ERROR } assert response.status_code == status.HTTP_400_BAD_REQUEST + assert re.match(REASON_PHRASE_RE, response.reason_phrase) assert sanitise_json_error(response.data) == expected + def test_400_validation_error(self): + request = factory.get('/') + response = ValidationErrorView.as_view()(request) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.reason_phrase == "{} ({})".format(STATUS_CODE_400, VALIDATION_ERROR) + assert response.data == [VALIDATION_ERROR] + class FunctionBasedViewIntegrationTests(TestCase): - def setUp(self): - self.view = basic_view def test_400_parse_error(self): request = factory.post('/', 'f00bar', content_type='application/json') - response = self.view(request) + response = basic_view(request) expected = { 'detail': JSON_ERROR } assert response.status_code == status.HTTP_400_BAD_REQUEST + assert re.match(REASON_PHRASE_RE, response.reason_phrase) assert sanitise_json_error(response.data) == expected + def test_400_validation_error(self): + request = factory.get('/') + response = validation_error_view(request) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.reason_phrase == "{} ({})".format(STATUS_CODE_400, VALIDATION_ERROR) + assert response.data == [VALIDATION_ERROR] + class TestCustomExceptionHandler(TestCase): def setUp(self):