aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework/response.py
blob: f8a512d30f4c2d8f71e4f7a0f7d62ed3ba5703f6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""
The :mod:`response` module provides :class:`Response` and :class:`ImmediateResponse` classes.

`Response` is a subclass of `HttpResponse`, and can be similarly instantiated and returned
from any view. It is a bit smarter than Django's `HttpResponse`, for it renders automatically
its content to a serial format by using a list of :mod:`renderers`.

To determine the content type to which it must render, default behaviour is to use standard
HTTP Accept header content negotiation. But `Response` also supports overriding the content type
by specifying an ``_accept=`` parameter in the URL. Also, `Response` will ignore `Accept` headers
from Internet Explorer user agents and use a sensible browser `Accept` header instead.


`ImmediateResponse` is an exception that inherits from `Response`. It can be used
to abort the request handling (i.e. ``View.get``, ``View.put``, ...),
and immediately returning a response.
"""

from django.template.response import SimpleTemplateResponse
from django.core.handlers.wsgi import STATUS_CODE_TEXT

from djangorestframework.utils.mediatypes import order_by_precedence
from djangorestframework.utils import MSIE_USER_AGENT_REGEX
from djangorestframework import status


class NotAcceptable(Exception):
    pass


class Response(SimpleTemplateResponse):
    """
    An HttpResponse that may include content that hasn't yet been serialized.

    Kwargs:
        - content(object). The raw content, not yet serialized.
        This must be native Python data that renderers can handle.
        (e.g.: `dict`, `str`, ...)
        - renderers(list/tuple). The renderers to use for rendering the response content.
    """

    _ACCEPT_QUERY_PARAM = '_accept'        # Allow override of Accept header in URL query params
    _IGNORE_IE_ACCEPT_HEADER = True

    def __init__(self, content=None, status=None, headers=None, view=None, request=None, renderers=None):
        # First argument taken by `SimpleTemplateResponse.__init__` is template_name,
        # which we don't need
        super(Response, self).__init__(None, status=status)

        self.raw_content = content
        self.has_content_body = content is not None
        self.headers = headers and headers[:] or []
        self.view = view
        self.request = request
        self.renderers = renderers

    def get_renderers(self):
        """
        Instantiates and returns the list of renderers the response will use.
        """
        return [renderer(self.view) for renderer in self.renderers]

    @property
    def rendered_content(self):
        """
        The final rendered content. Accessing this attribute triggers the
        complete rendering cycle: selecting suitable renderer, setting
        response's actual content type, rendering data.
        """
        renderer, media_type = self._determine_renderer()

        # Set the media type of the response
        self['Content-Type'] = renderer.media_type

        # Render the response content
        if self.has_content_body:
            return renderer.render(self.raw_content, media_type)
        return renderer.render()

    def render(self):
        try:
            return super(Response, self).render()
        except NotAcceptable:
            response = self._get_406_response()
            return response.render()

    @property
    def status_text(self):
        """
        Returns reason text corresponding to our HTTP response status code.
        Provided for convenience.
        """
        return STATUS_CODE_TEXT.get(self.status_code, '')

    def _determine_accept_list(self):
        """
        Returns a list of accepted media types. This list is determined from :

            1. overload with `_ACCEPT_QUERY_PARAM`
            2. `Accept` header of the request

        If those are useless, a default value is returned instead.
        """
        request = self.request

        if self._ACCEPT_QUERY_PARAM and request.GET.get(self._ACCEPT_QUERY_PARAM, None):
            # Use _accept parameter override
            return [request.GET.get(self._ACCEPT_QUERY_PARAM)]
        elif (self._IGNORE_IE_ACCEPT_HEADER and
              'HTTP_USER_AGENT' in request.META and
              MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
            # Ignore MSIE's broken accept behavior and do something sensible instead
            return ['text/html', '*/*']
        elif 'HTTP_ACCEPT' in request.META:
            # Use standard HTTP Accept negotiation
            return [token.strip() for token in request.META['HTTP_ACCEPT'].split(',')]
        else:
            # No accept header specified
            return ['*/*']

    def _determine_renderer(self):
        """
        Determines the appropriate renderer for the output, given the list of
        accepted media types, and the :attr:`renderers` set on this class.

        Returns a 2-tuple of `(renderer, media_type)`

        See: RFC 2616, Section 14
        http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
        """

        renderers = self.get_renderers()
        accepts = self._determine_accept_list()

        # Not acceptable response - Ignore accept header.
        if self.status_code == 406:
            return (renderers[0], renderers[0].media_type)

        # Check the acceptable media types against each renderer,
        # attempting more specific media types first
        # NB. The inner loop here isn't as bad as it first looks :)
        #     Worst case is we're looping over len(accept_list) * len(self.renderers)
        for media_type_list in order_by_precedence(accepts):
            for renderer in renderers:
                for media_type in media_type_list:
                    if renderer.can_handle_response(media_type):
                        return renderer, media_type

        # No acceptable renderers were found
        raise NotAcceptable

    def _get_406_response(self):
        renderer = self.renderers[0]
        return Response(
            {
                'detail': 'Could not satisfy the client\'s Accept header',
                'available_types': [renderer.media_type
                                    for renderer in self.renderers]
            },
            status=status.HTTP_406_NOT_ACCEPTABLE,
            view=self.view, request=self.request, renderers=[renderer])