diff --git a/django/views/debug.py b/django/views/debug.py index 1761d6904a..e6b7d4d821 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -48,12 +48,12 @@ def technical_500_response(request, exc_type, exc_value, tb, status_code=500): the values returned from sys.exc_info() and friends. """ reporter = get_exception_reporter_class(request)(request, exc_type, exc_value, tb) - if request.is_ajax(): - text = reporter.get_traceback_text() - return HttpResponse(text, status=status_code, content_type='text/plain; charset=utf-8') - else: + if request.accepts('text/html'): html = reporter.get_traceback_html() return HttpResponse(html, status=status_code, content_type='text/html') + else: + text = reporter.get_traceback_text() + return HttpResponse(text, status=status_code, content_type='text/plain; charset=utf-8') @functools.lru_cache() diff --git a/django/views/i18n.py b/django/views/i18n.py index 17eb4f9f61..23656a24db 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -33,7 +33,7 @@ def set_language(request): """ next_url = request.POST.get('next', request.GET.get('next')) if ( - (next_url or not request.is_ajax()) and + (next_url or request.accepts('text/html')) and not url_has_allowed_host_and_scheme( url=next_url, allowed_hosts={request.get_host()}, diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 3c88ecadd7..09b0b24eec 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -388,6 +388,11 @@ Miscellaneous Django 3.1, the first request to any previously cached template fragment will be a cache miss. +* The logic behind the decision to return a redirection fallback or a 204 HTTP + response from the :func:`~django.views.i18n.set_language` view is now based + on the ``Accept`` HTTP header instead of the ``X-Requested-With`` HTTP header + presence. + * The compatibility imports of ``django.core.exceptions.EmptyResultSet`` in ``django.db.models.query``, ``django.db.models.sql``, and ``django.db.models.sql.datastructures`` are removed. diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 241dcea660..1dc85d801c 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1803,9 +1803,18 @@ redirect to that URL will be performed. Otherwise, Django may fall back to redirecting the user to the URL from the ``Referer`` header or, if it is not set, to ``/``, depending on the nature of the request: -* For AJAX requests, the fallback will be performed only if the ``next`` - parameter was set. Otherwise a 204 status code (No Content) will be returned. -* For non-AJAX requests, the fallback will always be performed. +* If the request accepts HTML content (based on its ``Accept`` HTTP header), + the fallback will always be performed. + +* If the request doesn't accept HTML, the fallback will be performed only if + the ``next`` parameter was set. Otherwise a 204 status code (No Content) will + be returned. + +.. versionchanged:: 3.1 + + In older versions, the distinction for the fallback is based on whether the + ``X-Requested-With`` header is set to the value ``XMLHttpRequest``. This is + set by the jQuery ``ajax()`` method. Here's example HTML template code: diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 2556670904..59f36d44b8 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -159,11 +159,11 @@ Use the ``django.test.Client`` class to make requests. >>> c = Client() >>> c.get('/customers/details/', {'name': 'fred', 'age': 7}, - ... HTTP_X_REQUESTED_WITH='XMLHttpRequest') + ... HTTP_ACCEPT='application/json') - ...will send the HTTP header ``HTTP_X_REQUESTED_WITH`` to the - details view, which is a good way to test code paths that use the - :meth:`django.http.HttpRequest.is_ajax()` method. + ...will send the HTTP header ``HTTP_ACCEPT`` to the details view, which + is a good way to test code paths that use the + :meth:`django.http.HttpRequest.accepts()` method. .. admonition:: CGI specification diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 07d8208e19..63a9142862 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -1247,7 +1247,7 @@ class ExceptionReporterFilterTests(ExceptionReportTestMixin, LoggingCaptureMixin response = self.client.get( '/raises500/', HTTP_SECRET_HEADER='super_secret', - HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_ACCEPT='application/json', ) self.assertNotIn(b'super_secret', response.content) @@ -1289,17 +1289,17 @@ class CustomExceptionReporterFilterTests(SimpleTestCase): ) -class AjaxResponseExceptionReporterFilter(ExceptionReportTestMixin, LoggingCaptureMixin, SimpleTestCase): +class NonHTMLResponseExceptionReporterFilter(ExceptionReportTestMixin, LoggingCaptureMixin, SimpleTestCase): """ Sensitive information can be filtered out of error reports. - Here we specifically test the plain text 500 debug-only error page served - when it has been detected the request was sent by JS code. We don't check - for (non)existence of frames vars in the traceback information section of - the response content because we don't include them in these error pages. + The plain text 500 debug-only error page is served when it has been + detected the request doesn't accept HTML content. Don't check for + (non)existence of frames vars in the traceback information section of the + response content because they're not included in these error pages. Refs #14614. """ - rf = RequestFactory(HTTP_X_REQUESTED_WITH='XMLHttpRequest') + rf = RequestFactory(HTTP_ACCEPT='application/json') def test_non_sensitive_request(self): """ @@ -1346,8 +1346,8 @@ class AjaxResponseExceptionReporterFilter(ExceptionReportTestMixin, LoggingCaptu self.verify_unsafe_response(custom_exception_reporter_filter_view, check_for_vars=False) @override_settings(DEBUG=True, ROOT_URLCONF='view_tests.urls') - def test_ajax_response_encoding(self): - response = self.client.get('/raises500/', HTTP_X_REQUESTED_WITH='XMLHttpRequest') + def test_non_html_response_encoding(self): + response = self.client.get('/raises500/', HTTP_ACCEPT='application/json') self.assertEqual(response['Content-Type'], 'text/plain; charset=utf-8') diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index ff64bda29d..dd07aa9f93 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -111,11 +111,12 @@ class SetLanguageTests(TestCase): def test_setlang_performs_redirect_for_ajax_if_explicitly_requested(self): """ - The set_language view redirects to the "next" parameter for AJAX calls. + The set_language view redirects to the "next" parameter for requests + not accepting HTML response content. """ lang_code = self._get_inactive_language_code() post_data = {'language': lang_code, 'next': '/'} - response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.post('/i18n/setlang/', post_data, HTTP_ACCEPT='application/json') self.assertRedirects(response, '/') self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code) with ignore_warnings(category=RemovedInDjango40Warning): @@ -123,12 +124,12 @@ class SetLanguageTests(TestCase): def test_setlang_doesnt_perform_a_redirect_to_referer_for_ajax(self): """ - The set_language view doesn't redirect to the HTTP referer header for - AJAX calls. + The set_language view doesn't redirect to the HTTP referer header if + the request doesn't accept HTML response content. """ lang_code = self._get_inactive_language_code() post_data = {'language': lang_code} - headers = {'HTTP_REFERER': '/', 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} + headers = {'HTTP_REFERER': '/', 'HTTP_ACCEPT': 'application/json'} response = self.client.post('/i18n/setlang/', post_data, **headers) self.assertEqual(response.status_code, 204) self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code) @@ -137,11 +138,12 @@ class SetLanguageTests(TestCase): def test_setlang_doesnt_perform_a_default_redirect_for_ajax(self): """ - The set_language view returns 204 for AJAX calls by default. + The set_language view returns 204 by default for requests not accepting + HTML response content. """ lang_code = self._get_inactive_language_code() post_data = {'language': lang_code} - response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.post('/i18n/setlang/', post_data, HTTP_ACCEPT='application/json') self.assertEqual(response.status_code, 204) self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code) with ignore_warnings(category=RemovedInDjango40Warning): @@ -149,11 +151,12 @@ class SetLanguageTests(TestCase): def test_setlang_unsafe_next_for_ajax(self): """ - The fallback to root URL for the set_language view works for AJAX calls. + The fallback to root URL for the set_language view works for requests + not accepting HTML response content. """ lang_code = self._get_inactive_language_code() post_data = {'language': lang_code, 'next': '//unsafe/redirection/'} - response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.post('/i18n/setlang/', post_data, HTTP_ACCEPT='application/json') self.assertEqual(response.url, '/') self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)