class Response(object): """Represents an HTTP response to a client request. Note: `Response` is not meant to be instantiated directly by responders. Attributes: status (str): HTTP status line (e.g., '200 OK'). Falcon requires the full status line, not just the code (e.g., 200). This design makes the framework more efficient because it does not have to do any kind of conversion or lookup when composing the WSGI response. If not set explicitly, the status defaults to '200 OK'. Note: Falcon provides a number of constants for common status codes. They all start with the ``HTTP_`` prefix, as in: ``falcon.HTTP_204``. body (str or unicode): String representing response content. If Unicode, Falcon will encode as UTF-8 in the response. If data is already a byte string, use the data attribute instead (it's faster). body_encoded (bytes): Returns a UTF-8 encoded version of `body`. data (bytes): Byte string representing response content. Use this attribute in lieu of `body` when your content is already a byte string (``str`` or ``bytes`` in Python 2, or simply ``bytes`` in Python 3). See also the note below. Note: Under Python 2.x, if your content is of type ``str``, using the `data` attribute instead of `body` is the most efficient approach. However, if your text is of type ``unicode``, you will need to use the `body` attribute instead. Under Python 3.x, on the other hand, the 2.x ``str`` type can be thought of as having been replaced by what was once the ``unicode`` type, and so you will need to always use the `body` attribute for strings to ensure Unicode characters are properly encoded in the HTTP response. stream: Either a file-like object with a `read()` method that takes an optional size argument and returns a block of bytes, or an iterable object, representing response content, and yielding blocks as byte strings. Falcon will use *wsgi.file_wrapper*, if provided by the WSGI server, in order to efficiently serve file-like objects. stream_len (int): Expected length of `stream` (e.g., file size). """ __slots__ = ( '_body', # Stuff '_body_encoded', # Stuff 'data', '_headers', '_cookies', 'status', 'stream', 'stream_len') def __init__(self): self.status = '200 OK' self._headers = {} # NOTE(tbug): will be set to a SimpleCookie object # when cookie is set via set_cookie self._cookies = None self._body = None self._body_encoded = None self.data = None self.stream = None self.stream_len = None def _get_body(self): return self._body def _set_body(self, value): self._body = value self._body_encoded = None # NOTE(flaper87): Lets use a property # for the body in case its content was # encoded and then modified. body = property(_get_body, _set_body) @property def body_encoded(self): # NOTE(flaper87): Notice this property # is not thread-safe. If body is modified # before this property returns, we might # end up returning None. body = self._body if body and self._body_encoded is None: # NOTE(flaper87): Assume it is an # encoded str, then check and encode # if it isn't. self._body_encoded = body if isinstance(body, TEXT_TYPE): self._body_encoded = body.encode('utf-8') return self._body_encoded def set_stream(self, stream, stream_len): """Convenience method for setting both `stream` and `stream_len`. Although the `stream` and `stream_len` properties may be set directly, using this method ensures `stream_len` is not accidentally neglected. """ self.stream = stream self.stream_len = stream_len def set_cookie(self, name, value, expires=None, max_age=None, domain=None, path=None, secure=True, http_only=True): """Set a response cookie. Note: This method can be called multiple times to add one or more cookies to the response. See Also: To learn more about setting cookies, see :ref:`Setting Cookies <setting-cookies>`. The parameters listed below correspond to those defined in `RFC 6265`_. Args: name (str): Cookie name value (str): Cookie value expires (datetime): Specifies when the cookie should expire. By default, cookies expire when the user agent exits. max_age (int): Defines the lifetime of the cookie in seconds. After the specified number of seconds elapse, the client should discard the cookie. domain (str): Specifies the domain for which the cookie is valid. An explicitly specified domain must always start with a dot. A value of 0 means the cookie should be discarded immediately. path (str): Specifies the subset of URLs to which this cookie applies. secure (bool): Direct the client to use only secure means to contact the origin server whenever it sends back this cookie (default: ``True``). Warning: You will also need to enforce HTTPS for the cookies to be transfered securely. http_only (bool): Direct the client to only transfer the cookie with unscripted HTTP requests (default: ``True``). This is intended to mitigate some forms of cross-site scripting. Raises: KeyError: `name` is not a valid cookie name. ValueError: `value` is not a valid cookie value. .. _RFC 6265: http://tools.ietf.org/html/rfc6265 """ if not is_ascii_encodable(name): raise KeyError('"name" is not ascii encodable') if not is_ascii_encodable(value): raise ValueError('"value" is not ascii encodable') if PY2: # pragma: no cover name = str(name) value = str(value) if self._cookies is None: self._cookies = SimpleCookie() try: self._cookies[name] = value except CookieError as e: # pragma: no cover # NOTE(tbug): we raise a KeyError here, to avoid leaking # the CookieError to the user. SimpleCookie (well, BaseCookie) # only throws CookieError on issues with the cookie key raise KeyError(str(e)) if expires: # set Expires on cookie. Format is Wdy, DD Mon YYYY HH:MM:SS GMT # NOTE(tbug): we never actually need to # know that GMT is named GMT when formatting cookies. # It is a function call less to just write "GMT" in the fmt string: fmt = "%a, %d %b %Y %H:%M:%S GMT" if expires.tzinfo is None: # naive self._cookies[name]["expires"] = expires.strftime(fmt) else: # aware gmt_expires = expires.astimezone(GMT_TIMEZONE) self._cookies[name]["expires"] = gmt_expires.strftime(fmt) if max_age: self._cookies[name]["max-age"] = max_age if domain: self._cookies[name]["domain"] = domain if path: self._cookies[name]["path"] = path if secure: self._cookies[name]["secure"] = secure if http_only: self._cookies[name]["httponly"] = http_only def unset_cookie(self, name): """Unset a cookie in the response Note: This will clear the contents of the cookie, and instruct the browser to immediately expire its own copy of the cookie, if any. """ if self._cookies is None: self._cookies = SimpleCookie() self._cookies[name] = "" # NOTE(Freezerburn): SimpleCookie apparently special cases the # expires attribute to automatically use strftime and set the # time as a delta from the current time. We use -1 here to # basically tell the browser to immediately expire the cookie, # thus removing it from future request objects. self._cookies[name]["expires"] = -1 def get_header(self, name): """Retrieve the raw string value for the given header. Args: name (str): Header name, case-insensitive. Must be of type ``str`` or ``StringType``, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. Returns: str: The header's value if set, otherwise ``None``. """ return self._headers.get(name.lower(), None) def set_header(self, name, value): """Set a header for this response to a given value. Warning: Calling this method overwrites the existing value, if any. Warning: For setting cookies, see instead :meth:`~.set_cookie` Args: name (str): Header name (case-insensitive). The restrictions noted below for the header's value also apply here. value (str): Value for the header. Must be of type ``str`` or ``StringType`` and contain only ISO-8859-1 characters. Under Python 2.x, the ``unicode`` type is also accepted, although such strings are also limited to ISO-8859-1. """ name, value = self._encode_header(name, value) # NOTE(kgriffs): normalize name by lowercasing it self._headers[name.lower()] = value def append_header(self, name, value): """Set or append a header for this response. Warning: If the header already exists, the new value will be appended to it, delimited by a comma. Most header specifications support this format, Cookie and Set-Cookie being the notable exceptions. Warning: For setting cookies, see :py:meth:`~.set_cookie` Args: name (str): Header name (case-insensitive). The restrictions noted below for the header's value also apply here. value (str): Value for the header. Must be of type ``str`` or ``StringType`` and contain only ISO-8859-1 characters. Under Python 2.x, the ``unicode`` type is also accepted, although such strings are also limited to ISO-8859-1. """ name, value = self._encode_header(name, value) name = name.lower() if name in self._headers: value = self._headers[name] + ',' + value self._headers[name] = value def set_headers(self, headers): """Set several headers at once. Warning: Calling this method overwrites existing values, if any. Args: headers (dict or list): A dictionary of header names and values to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and *value* must be of type ``str`` or ``StringType`` and contain only ISO-8859-1 characters. Under Python 2.x, the ``unicode`` type is also accepted, although such strings are also limited to ISO-8859-1. Note: Falcon can process a list of tuples slightly faster than a dict. Raises: ValueError: `headers` was not a ``dict`` or ``list`` of ``tuple``. """ if isinstance(headers, dict): headers = headers.items() # NOTE(kgriffs): We can't use dict.update because we have to # normalize the header names. _headers = self._headers for name, value in headers: name, value = self._encode_header(name, value) _headers[name.lower()] = value def add_link(self, target, rel, title=None, title_star=None, anchor=None, hreflang=None, type_hint=None): """ Add a link header to the response. See also: https://tools.ietf.org/html/rfc5988 Note: Calling this method repeatedly will cause each link to be appended to the Link header value, separated by commas. Note: So-called "link-extension" elements, as defined by RFC 5988, are not yet supported. See also Issue #288. Args: target (str): Target IRI for the resource identified by the link. Will be converted to a URI, if necessary, per RFC 3987, Section 3.1. rel (str): Relation type of the link, such as "next" or "bookmark". See also http://goo.gl/618GHr for a list of registered link relation types. Kwargs: title (str): Human-readable label for the destination of the link (default ``None``). If the title includes non-ASCII characters, you will need to use `title_star` instead, or provide both a US-ASCII version using `title` and a Unicode version using `title_star`. title_star (tuple of str): Localized title describing the destination of the link (default ``None``). The value must be a two-member tuple in the form of (*language-tag*, *text*), where *language-tag* is a standard language identifier as defined in RFC 5646, Section 2.1, and *text* is a Unicode string. Note: *language-tag* may be an empty string, in which case the client will assume the language from the general context of the current request. Note: *text* will always be encoded as UTF-8. If the string contains non-ASCII characters, it should be passed as a ``unicode`` type string (requires the 'u' prefix in Python 2). anchor (str): Override the context IRI with a different URI (default None). By default, the context IRI for the link is simply the IRI of the requested resource. The value provided may be a relative URI. hreflang (str or iterable): Either a single *language-tag*, or a ``list`` or ``tuple`` of such tags to provide a hint to the client as to the language of the result of following the link. A list of tags may be given in order to indicate to the client that the target resource is available in multiple languages. type_hint(str): Provides a hint as to the media type of the result of dereferencing the link (default ``None``). As noted in RFC 5988, this is only a hint and does not override the Content-Type header returned when the link is followed. """ # PERF(kgriffs): Heuristic to detect possiblity of an extension # relation type, in which case it will be a URL that may contain # reserved characters. Otherwise, don't waste time running the # string through uri.encode # # Example values for rel: # # "next" # "http://example.com/ext-type" # "https://example.com/ext-type" # "alternate http://example.com/ext-type" # "http://example.com/ext-type alternate" # if '//' in rel: if ' ' in rel: rel = ('"' + ' '.join([uri_encode(r) for r in rel.split()]) + '"') else: rel = '"' + uri_encode(rel) + '"' value = '<' + uri_encode(target) + '>; rel=' + rel if title is not None: value += '; title="' + title + '"' if title_star is not None: value += ("; title*=UTF-8'" + title_star[0] + "'" + uri_encode_value(title_star[1])) if type_hint is not None: value += '; type="' + type_hint + '"' if hreflang is not None: if isinstance(hreflang, STRING_TYPES): value += '; hreflang=' + hreflang else: value += '; ' value += '; '.join(['hreflang=' + lang for lang in hreflang]) if anchor is not None: value += '; anchor="' + uri_encode(anchor) + '"' _headers = self._headers if 'link' in _headers: _headers['link'] += ', ' + value else: _headers['link'] = value cache_control = header_property( 'Cache-Control', """Sets the Cache-Control header. Used to set a list of cache directives to use as the value of the Cache-Control header. The list will be joined with ", " to produce the value for the header. """, lambda v: ', '.join(v)) content_location = header_property('Content-Location', 'Sets the Content-Location header.', uri_encode) content_range = header_property( 'Content-Range', """A tuple to use in constructing a value for the Content-Range header. The tuple has the form (*start*, *end*, *length*), where *start* and *end* designate the byte range (inclusive), and *length* is the total number of bytes, or '\*' if unknown. You may pass ``int``'s for these numbers (no need to convert to ``str`` beforehand). Note: You only need to use the alternate form, 'bytes \*/1234', for responses that use the status '416 Range Not Satisfiable'. In this case, raising ``falcon.HTTPRangeNotSatisfiable`` will do the right thing. See also: http://goo.gl/Iglhp """, format_range) content_type = header_property('Content-Type', 'Sets the Content-Type header.') etag = header_property('ETag', 'Sets the ETag header.') last_modified = header_property( 'Last-Modified', """Sets the Last-Modified header. Set to a ``datetime`` (UTC) instance. Note: Falcon will format the ``datetime`` as an HTTP date string. """, dt_to_http) location = header_property('Location', 'Sets the Location header.', uri_encode) retry_after = header_property( 'Retry-After', """Sets the Retry-After header. The expected value is an integral number of seconds to use as the value for the header. The HTTP-date syntax is not supported. """, str) vary = header_property( 'Vary', """Value to use for the Vary header. Set this property to an iterable of header names. For a single asterisk or field value, simply pass a single-element ``list`` or ``tuple``. "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." (Wikipedia) See also: http://goo.gl/NGHdL """, lambda v: ', '.join(v)) def _encode_header(self, name, value, py2=PY2): if py2: # pragma: no cover if isinstance(name, unicode): name = name.encode('ISO-8859-1') if isinstance(value, unicode): value = value.encode('ISO-8859-1') return name, value def _wsgi_headers(self, media_type=None, py2=PY2): """Convert headers into the format expected by WSGI servers. Args: media_type: Default media type to use for the Content-Type header if the header was not set explicitly (default ``None``). """ headers = self._headers # PERF(kgriffs): Using "in" like this is faster than using # dict.setdefault (tested on py27). set_content_type = (media_type is not None and 'content-type' not in headers) if set_content_type: headers['content-type'] = media_type if py2: # pragma: no cover # PERF(kgriffs): Don't create an extra list object if # it isn't needed. items = headers.items() else: items = list(headers.items()) # pragma: no cover if self._cookies is not None: # PERF(tbug): # The below implementation is ~23% faster than # the alternative: # # self._cookies.output().split("\\r\\n") # # Even without the .split("\\r\\n"), the below # is still ~17% faster, so don't use .output() items += [("set-cookie", c.OutputString()) for c in self._cookies.values()] return items
class Response: """Represents an HTTP response to a client request. Note: ``Response`` is not meant to be instantiated directly by responders. Keyword Arguments: options (dict): Set of global options passed from the App handler. Attributes: status: HTTP status code or line (e.g., ``'200 OK'``). This may be set to a member of :class:`http.HTTPStatus`, an HTTP status line string or byte string (e.g., ``'200 OK'``), or an ``int``. Note: The Falcon framework itself provides a number of constants for common status codes. They all start with the ``HTTP_`` prefix, as in: ``falcon.HTTP_204``. (See also: :ref:`status`.) media (object): A serializable object supported by the media handlers configured via :class:`falcon.RequestOptions`. Note: See also :ref:`media` for more information regarding media handling. text (str): String representing response content. Note: Falcon will encode the given text as UTF-8 in the response. If the content is already a byte string, use the :attr:`data` attribute instead (it's faster). body (str): Deprecated alias for :attr:`text`. Will be removed in a future Falcon version. data (bytes): Byte string representing response content. Use this attribute in lieu of `text` when your content is already a byte string (of type ``bytes``). See also the note below. Warning: Always use the `text` attribute for text, or encode it first to ``bytes`` when using the `data` attribute, to ensure Unicode characters are properly encoded in the HTTP response. stream: Either a file-like object with a `read()` method that takes an optional size argument and returns a block of bytes, or an iterable object, representing response content, and yielding blocks as byte strings. Falcon will use *wsgi.file_wrapper*, if provided by the WSGI server, in order to efficiently serve file-like objects. Note: If the stream is set to an iterable object that requires resource cleanup, it can implement a close() method to do so. The close() method will be called upon completion of the request. context (object): Empty object to hold any data (in its attributes) about the response which is specific to your app (e.g. session object). Falcon itself will not interact with this attribute after it has been initialized. Note: **New in 2.0:** The default `context_type` (see below) was changed from :class:`dict` to a bare class; the preferred way to pass response-specific data is now to set attributes directly on the `context` object. For example:: resp.context.cache_strategy = 'lru' context_type (class): Class variable that determines the factory or type to use for initializing the `context` attribute. By default, the framework will instantiate bare objects (instances of the bare :class:`falcon.Context` class). However, you may override this behavior by creating a custom child class of :class:`falcon.Response`, and then passing that new class to ``falcon.App()`` by way of the latter's `response_type` parameter. Note: When overriding `context_type` with a factory function (as opposed to a class), the function is called like a method of the current Response instance. Therefore the first argument is the Response instance itself (self). options (dict): Set of global options passed from the App handler. headers (dict): Copy of all headers set for the response, sans cookies. Note that a new copy is created and returned each time this property is referenced. complete (bool): Set to ``True`` from within a middleware method to signal to the framework that request processing should be short-circuited (see also :ref:`Middleware <middleware>`). """ __slots__ = ( 'text', 'context', 'options', 'status', 'stream', '_cookies', '_data', '_extra_headers', '_headers', '_media', '_media_rendered', '__dict__', ) complete = False # Child classes may override this context_type = structures.Context def __init__(self, options=None): self.status = '200 OK' self._headers = {} # NOTE(kgriffs): Collection of additional headers as a list of raw # tuples, to use in cases where we need more control over setting # headers and duplicates are allowable or even necessary. # # PERF(kgriffs): Save some CPU cycles and a few bytes of RAM by # only instantiating the list object later on IFF it is needed. self._extra_headers = None self.options = options if options else ResponseOptions() # NOTE(tbug): will be set to a SimpleCookie object # when cookie is set via set_cookie self._cookies = None self.text = None self.stream = None self._data = None self._media = None self._media_rendered = _UNSET self.context = self.context_type() @property # type: ignore @deprecated('Please use text instead.', is_property=True) def body(self): return self.text @body.setter # type: ignore @deprecated('Please use text instead.', is_property=True) def body(self, value): self.text = value @property def data(self): return self._data @data.setter def data(self, value): self._data = value @property def headers(self): return self._headers.copy() @property def media(self): return self._media @media.setter def media(self, value): self._media = value self._media_rendered = _UNSET @property def stream_len(self): # NOTE(kgriffs): Provide some additional information by raising the # error explicitly. raise AttributeError(_STREAM_LEN_REMOVED_MSG) @stream_len.setter def stream_len(self, value): # NOTE(kgriffs): We explicitly disallow setting the deprecated attribute # so that apps relying on it do not fail silently. raise AttributeError(_STREAM_LEN_REMOVED_MSG) def render_body(self): """Get the raw bytestring content for the response body. This method returns the raw data for the HTTP response body, taking into account the :attr:`~.text`, :attr:`~.data`, and :attr:`~.media` attributes. Note: This method ignores :attr:`~.stream`; the caller must check and handle that attribute directly. Returns: bytes: The UTF-8 encoded value of the `text` attribute, if set. Otherwise, the value of the `data` attribute if set, or finally the serialized value of the `media` attribute. If none of these attributes are set, ``None`` is returned. """ text = self.text if text is None: data = self._data if data is None and self._media is not None: # NOTE(kgriffs): We use a special _UNSET singleton since # None is ambiguous (the media handler might return None). if self._media_rendered is _UNSET: if not self.content_type: self.content_type = self.options.default_media_type handler = self.options.media_handlers.find_by_media_type( self.content_type, self.options.default_media_type) self._media_rendered = handler.serialize( self._media, self.content_type) data = self._media_rendered else: try: # NOTE(kgriffs): Normally we expect text to be a string data = text.encode() except AttributeError: # NOTE(kgriffs): Assume it was a bytes object already data = text return data def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.status) def set_stream(self, stream, content_length): """Set both `stream` and `content_length`. Although the :attr:`~falcon.Response.stream` and :attr:`~falcon.Response.content_length` properties may be set directly, using this method ensures :attr:`~falcon.Response.content_length` is not accidentally neglected when the length of the stream is known in advance. Using this method is also slightly more performant as compared to setting the properties individually. Note: If the stream length is unknown, you can set :attr:`~falcon.Response.stream` directly, and ignore :attr:`~falcon.Response.content_length`. In this case, the ASGI server may choose to use chunked encoding or one of the other strategies suggested by PEP-3333. Args: stream: A readable file-like object. content_length (int): Length of the stream, used for the Content-Length header in the response. """ self.stream = stream # PERF(kgriffs): Set directly rather than incur the overhead of # the self.content_length property. self._headers['content-length'] = str(content_length) def set_cookie(self, name, value, expires=None, max_age=None, domain=None, path=None, secure=None, http_only=True, same_site=None): """Set a response cookie. Note: This method can be called multiple times to add one or more cookies to the response. See Also: To learn more about setting cookies, see :ref:`Setting Cookies <setting-cookies>`. The parameters listed below correspond to those defined in `RFC 6265`_. Args: name (str): Cookie name value (str): Cookie value Keyword Args: expires (datetime): Specifies when the cookie should expire. By default, cookies expire when the user agent exits. (See also: RFC 6265, Section 4.1.2.1) max_age (int): Defines the lifetime of the cookie in seconds. By default, cookies expire when the user agent exits. If both `max_age` and `expires` are set, the latter is ignored by the user agent. Note: Coercion to ``int`` is attempted if provided with ``float`` or ``str``. (See also: RFC 6265, Section 4.1.2.2) domain (str): Restricts the cookie to a specific domain and any subdomains of that domain. By default, the user agent will return the cookie only to the origin server. When overriding this default behavior, the specified domain must include the origin server. Otherwise, the user agent will reject the cookie. Note: Cookies do not provide isolation by port, so the domain should not provide one. (See also: RFC 6265, Section 8.5) (See also: RFC 6265, Section 4.1.2.3) path (str): Scopes the cookie to the given path plus any subdirectories under that path (the "/" character is interpreted as a directory separator). If the cookie does not specify a path, the user agent defaults to the path component of the requested URI. Warning: User agent interfaces do not always isolate cookies by path, and so this should not be considered an effective security measure. (See also: RFC 6265, Section 4.1.2.4) secure (bool): Direct the client to only return the cookie in subsequent requests if they are made over HTTPS (default: ``True``). This prevents attackers from reading sensitive cookie data. Note: The default value for this argument is normally ``True``, but can be modified by setting :py:attr:`~.ResponseOptions.secure_cookies_by_default` via :any:`App.resp_options`. Warning: For the `secure` cookie attribute to be effective, your application will need to enforce HTTPS. (See also: RFC 6265, Section 4.1.2.5) http_only (bool): The HttpOnly attribute limits the scope of the cookie to HTTP requests. In particular, the attribute instructs the user agent to omit the cookie when providing access to cookies via "non-HTTP" APIs. This is intended to mitigate some forms of cross-site scripting. (default: ``True``) Note: HttpOnly cookies are not visible to javascript scripts in the browser. They are automatically sent to the server on javascript ``XMLHttpRequest`` or ``Fetch`` requests. (See also: RFC 6265, Section 4.1.2.6) same_site (str): Helps protect against CSRF attacks by restricting when a cookie will be attached to the request by the user agent. When set to ``'Strict'``, the cookie will only be sent along with "same-site" requests. If the value is ``'Lax'``, the cookie will be sent with same-site requests, and with "cross-site" top-level navigations. If the value is ``'None'``, the cookie will be sent with same-site and cross-site requests. Finally, when this attribute is not set on the cookie, the attribute will be treated as if it had been set to ``'None'``. (See also: `Same-Site RFC Draft`_) Raises: KeyError: `name` is not a valid cookie name. ValueError: `value` is not a valid cookie value. .. _RFC 6265: http://tools.ietf.org/html/rfc6265 .. _Same-Site RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7 """ if not is_ascii_encodable(name): raise KeyError('name is not ascii encodable') if not is_ascii_encodable(value): raise ValueError('value is not ascii encodable') value = str(value) if self._cookies is None: self._cookies = http_cookies.SimpleCookie() try: self._cookies[name] = value except http_cookies.CookieError as e: # pragma: no cover # NOTE(tbug): we raise a KeyError here, to avoid leaking # the CookieError to the user. SimpleCookie (well, BaseCookie) # only throws CookieError on issues with the cookie key raise KeyError(str(e)) if expires: # set Expires on cookie. Format is Wdy, DD Mon YYYY HH:MM:SS GMT # NOTE(tbug): we never actually need to # know that GMT is named GMT when formatting cookies. # It is a function call less to just write "GMT" in the fmt string: fmt = '%a, %d %b %Y %H:%M:%S GMT' if expires.tzinfo is None: # naive self._cookies[name]['expires'] = expires.strftime(fmt) else: # aware gmt_expires = expires.astimezone(GMT_TIMEZONE) self._cookies[name]['expires'] = gmt_expires.strftime(fmt) if max_age: # RFC 6265 section 5.2.2 says about the max-age value: # "If the remainder of attribute-value contains a non-DIGIT # character, ignore the cookie-av." # That is, RFC-compliant response parsers will ignore the max-age # attribute if the value contains a dot, as in floating point # numbers. Therefore, attempt to convert the value to an integer. self._cookies[name]['max-age'] = int(max_age) if domain: self._cookies[name]['domain'] = domain if path: self._cookies[name]['path'] = path is_secure = self.options.secure_cookies_by_default if secure is None else secure if is_secure: self._cookies[name]['secure'] = True if http_only: self._cookies[name]['httponly'] = http_only # PERF(kgriffs): Morsel.__setitem__() will lowercase this anyway, # so we can just pass this in and when __setitem__() calls # lower() it will be very slightly faster. if same_site: same_site = same_site.lower() if same_site not in _RESERVED_SAMESITE_VALUES: raise ValueError( "same_site must be set to either 'lax', 'strict', or 'none'" ) self._cookies[name]['samesite'] = same_site.capitalize() def unset_cookie(self, name, domain=None, path=None): """Unset a cookie in the response. Clears the contents of the cookie, and instructs the user agent to immediately expire its own copy of the cookie. Note: Modern browsers place restriction on cookies without the "same-site" cookie attribute set. To that end this attribute is set to ``'Lax'`` by this method. (See also: `Same-Site warnings`_) Warning: In order to successfully remove a cookie, both the path and the domain must match the values that were used when the cookie was created. Args: name (str): Cookie name Keyword Args: domain (str): Restricts the cookie to a specific domain and any subdomains of that domain. By default, the user agent will return the cookie only to the origin server. When overriding this default behavior, the specified domain must include the origin server. Otherwise, the user agent will reject the cookie. Note: Cookies do not provide isolation by port, so the domain should not provide one. (See also: RFC 6265, Section 8.5) (See also: RFC 6265, Section 4.1.2.3) path (str): Scopes the cookie to the given path plus any subdirectories under that path (the "/" character is interpreted as a directory separator). If the cookie does not specify a path, the user agent defaults to the path component of the requested URI. Warning: User agent interfaces do not always isolate cookies by path, and so this should not be considered an effective security measure. (See also: RFC 6265, Section 4.1.2.4) .. _Same-Site warnings: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#Fixing_common_warnings """ # noqa: E501 if self._cookies is None: self._cookies = http_cookies.SimpleCookie() self._cookies[name] = '' # NOTE(Freezerburn): SimpleCookie apparently special cases the # expires attribute to automatically use strftime and set the # time as a delta from the current time. We use -1 here to # basically tell the browser to immediately expire the cookie, # thus removing it from future request objects. self._cookies[name]['expires'] = -1 # NOTE(CaselIT): Set SameSite to Lax to avoid setting invalid cookies. # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#Fixing_common_warnings # noqa: E501 self._cookies[name]['samesite'] = 'Lax' if domain: self._cookies[name]['domain'] = domain if path: self._cookies[name]['path'] = path def get_header(self, name, default=None): """Retrieve the raw string value for the given header. Normally, when a header has multiple values, they will be returned as a single, comma-delimited string. However, the Set-Cookie header does not support this format, and so attempting to retrieve it will raise an error. Args: name (str): Header name, case-insensitive. Must be of type ``str`` or ``StringType``, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. Keyword Args: default: Value to return if the header is not found (default ``None``). Raises: ValueError: The value of the 'Set-Cookie' header(s) was requested. Returns: str: The value of the specified header if set, or the default value if not set. """ # NOTE(kgriffs): normalize name by lowercasing it name = name.lower() if name == 'set-cookie': raise HeaderNotSupported( 'Getting Set-Cookie is not currently supported.') return self._headers.get(name, default) def set_header(self, name, value): """Set a header for this response to a given value. Warning: Calling this method overwrites any values already set for this header. To append an additional value for this header, use :meth:`~.append_header` instead. Warning: This method cannot be used to set cookies; instead, use :meth:`~.append_header` or :meth:`~.set_cookie`. Args: name (str): Header name (case-insensitive). The name may contain only US-ASCII characters. value (str): Value for the header. As with the header's name, the value may contain only US-ASCII characters. Raises: ValueError: `name` cannot be ``'Set-Cookie'``. """ # NOTE(kgriffs): uwsgi fails with a TypeError if any header # is not a str, so do the conversion here. It's actually # faster to not do an isinstance check. str() will encode # to US-ASCII. value = str(value) # NOTE(kgriffs): normalize name by lowercasing it name = name.lower() if name == 'set-cookie': raise HeaderNotSupported( 'This method cannot be used to set cookies') self._headers[name] = value def delete_header(self, name): """Delete a header that was previously set for this response. If the header was not previously set, nothing is done (no error is raised). Otherwise, all values set for the header will be removed from the response. Note that calling this method is equivalent to setting the corresponding header property (when said property is available) to ``None``. For example:: resp.etag = None Warning: This method cannot be used with the Set-Cookie header. Instead, use :meth:`~.unset_cookie` to remove a cookie and ensure that the user agent expires its own copy of the data as well. Args: name (str): Header name (case-insensitive). The name may contain only US-ASCII characters. Raises: ValueError: `name` cannot be ``'Set-Cookie'``. """ # NOTE(kgriffs): normalize name by lowercasing it name = name.lower() if name == 'set-cookie': raise HeaderNotSupported( 'This method cannot be used to remove cookies') self._headers.pop(name, None) def append_header(self, name, value): """Set or append a header for this response. If the header already exists, the new value will normally be appended to it, delimited by a comma. The notable exception to this rule is Set-Cookie, in which case a separate header line for each value will be included in the response. Note: While this method can be used to efficiently append raw Set-Cookie headers to the response, you may find :py:meth:`~.set_cookie` to be more convenient. Args: name (str): Header name (case-insensitive). The name may contain only US-ASCII characters. value (str): Value for the header. As with the header's name, the value may contain only US-ASCII characters. """ # NOTE(kgriffs): uwsgi fails with a TypeError if any header # is not a str, so do the conversion here. It's actually # faster to not do an isinstance check. str() will encode # to US-ASCII. value = str(value) # NOTE(kgriffs): normalize name by lowercasing it name = name.lower() if name == 'set-cookie': if not self._extra_headers: self._extra_headers = [(name, value)] else: self._extra_headers.append((name, value)) else: if name in self._headers: value = self._headers[name] + ', ' + value self._headers[name] = value def set_headers(self, headers): """Set several headers at once. This method can be used to set a collection of raw header names and values all at once. Warning: Calling this method overwrites any existing values for the given header. If a list containing multiple instances of the same header is provided, only the last value will be used. To add multiple values to the response for a given header, see :meth:`~.append_header`. Warning: This method cannot be used to set cookies; instead, use :meth:`~.append_header` or :meth:`~.set_cookie`. Args: headers (Iterable[[str, str]]): An iterable of ``[name, value]`` two-member iterables, or a dict-like object that implements an ``items()`` method. Both *name* and *value* must be of type ``str`` and contain only US-ASCII characters. Note: Falcon can process an iterable of tuples slightly faster than a dict. Raises: ValueError: `headers` was not a ``dict`` or ``list`` of ``tuple`` or ``Iterable[[str, str]]``. """ header_items = getattr(headers, 'items', None) if callable(header_items): headers = header_items() # NOTE(kgriffs): We can't use dict.update because we have to # normalize the header names. _headers = self._headers for name, value in headers: # NOTE(kgriffs): uwsgi fails with a TypeError if any header # is not a str, so do the conversion here. It's actually # faster to not do an isinstance check. str() will encode # to US-ASCII. value = str(value) name = name.lower() if name == 'set-cookie': raise HeaderNotSupported( 'This method cannot be used to set cookies') _headers[name] = value def append_link(self, target, rel, title=None, title_star=None, anchor=None, hreflang=None, type_hint=None, crossorigin=None): """Append a link header to the response. (See also: RFC 5988, Section 1) Note: Calling this method repeatedly will cause each link to be appended to the Link header value, separated by commas. Note: So-called "link-extension" elements, as defined by RFC 5988, are not yet supported. See also Issue #288. Args: target (str): Target IRI for the resource identified by the link. Will be converted to a URI, if necessary, per RFC 3987, Section 3.1. rel (str): Relation type of the link, such as "next" or "bookmark". (See also: http://www.iana.org/assignments/link-relations/link-relations.xhtml) Keyword Args: title (str): Human-readable label for the destination of the link (default ``None``). If the title includes non-ASCII characters, you will need to use `title_star` instead, or provide both a US-ASCII version using `title` and a Unicode version using `title_star`. title_star (tuple of str): Localized title describing the destination of the link (default ``None``). The value must be a two-member tuple in the form of (*language-tag*, *text*), where *language-tag* is a standard language identifier as defined in RFC 5646, Section 2.1, and *text* is a Unicode string. Note: *language-tag* may be an empty string, in which case the client will assume the language from the general context of the current request. Note: *text* will always be encoded as UTF-8. anchor (str): Override the context IRI with a different URI (default None). By default, the context IRI for the link is simply the IRI of the requested resource. The value provided may be a relative URI. hreflang (str or iterable): Either a single *language-tag*, or a ``list`` or ``tuple`` of such tags to provide a hint to the client as to the language of the result of following the link. A list of tags may be given in order to indicate to the client that the target resource is available in multiple languages. type_hint(str): Provides a hint as to the media type of the result of dereferencing the link (default ``None``). As noted in RFC 5988, this is only a hint and does not override the Content-Type header returned when the link is followed. crossorigin(str): Determines how cross origin requests are handled. Can take values 'anonymous' or 'use-credentials' or None. (See: https://www.w3.org/TR/html50/infrastructure.html#cors-settings-attribute) """ # PERF(kgriffs): Heuristic to detect possiblity of an extension # relation type, in which case it will be a URL that may contain # reserved characters. Otherwise, don't waste time running the # string through uri.encode # # Example values for rel: # # "next" # "http://example.com/ext-type" # "https://example.com/ext-type" # "alternate http://example.com/ext-type" # "http://example.com/ext-type alternate" # if '//' in rel: if ' ' in rel: rel = ('"' + ' '.join([uri_encode(r) for r in rel.split()]) + '"') else: rel = '"' + uri_encode(rel) + '"' value = '<' + uri_encode(target) + '>; rel=' + rel if title is not None: value += '; title="' + title + '"' if title_star is not None: value += ("; title*=UTF-8'" + title_star[0] + "'" + uri_encode_value(title_star[1])) if type_hint is not None: value += '; type="' + type_hint + '"' if hreflang is not None: if isinstance(hreflang, str): value += '; hreflang=' + hreflang else: value += '; ' value += '; '.join(['hreflang=' + lang for lang in hreflang]) if anchor is not None: value += '; anchor="' + uri_encode(anchor) + '"' if crossorigin is not None: crossorigin = crossorigin.lower() if crossorigin not in _RESERVED_CROSSORIGIN_VALUES: raise ValueError('crossorigin must be set to either ' "'anonymous' or 'use-credentials'") if crossorigin == 'anonymous': value += '; crossorigin' else: # crossorigin == 'use-credentials' # PERF(vytas): the only remaining value is inlined. # Un-inline in case more values are supported in the future. value += '; crossorigin="use-credentials"' _headers = self._headers if 'link' in _headers: _headers['link'] += ', ' + value else: _headers['link'] = value # NOTE(kgriffs): Alias deprecated as of 3.0 add_link = append_link cache_control = header_property( 'Cache-Control', """Set the Cache-Control header. Used to set a list of cache directives to use as the value of the Cache-Control header. The list will be joined with ", " to produce the value for the header. """, format_header_value_list) content_location = header_property( 'Content-Location', """Set the Content-Location header. This value will be URI encoded per RFC 3986. If the value that is being set is already URI encoded it should be decoded first or the header should be set manually using the set_header method. """, uri_encode) content_length = header_property( 'Content-Length', """Set the Content-Length header. This property can be used for responding to HEAD requests when you aren't actually providing the response body, or when streaming the response. If either the `text` property or the `data` property is set on the response, the framework will force Content-Length to be the length of the given text bytes. Therefore, it is only necessary to manually set the content length when those properties are not used. Note: In cases where the response content is a stream (readable file-like object), Falcon will not supply a Content-Length header to the server unless `content_length` is explicitly set. Consequently, the server may choose to use chunked encoding in this case. """, ) content_range = header_property( 'Content-Range', """A tuple to use in constructing a value for the Content-Range header. The tuple has the form (*start*, *end*, *length*, [*unit*]), where *start* and *end* designate the range (inclusive), and *length* is the total length, or '\\*' if unknown. You may pass ``int``'s for these numbers (no need to convert to ``str`` beforehand). The optional value *unit* describes the range unit and defaults to 'bytes' Note: You only need to use the alternate form, 'bytes \\*/1234', for responses that use the status '416 Range Not Satisfiable'. In this case, raising ``falcon.HTTPRangeNotSatisfiable`` will do the right thing. (See also: RFC 7233, Section 4.2) """, format_range) content_type = header_property( 'Content-Type', """Sets the Content-Type header. The ``falcon`` module provides a number of constants for common media types, including ``falcon.MEDIA_JSON``, ``falcon.MEDIA_MSGPACK``, ``falcon.MEDIA_YAML``, ``falcon.MEDIA_XML``, ``falcon.MEDIA_HTML``, ``falcon.MEDIA_JS``, ``falcon.MEDIA_TEXT``, ``falcon.MEDIA_JPEG``, ``falcon.MEDIA_PNG``, and ``falcon.MEDIA_GIF``. """) downloadable_as = header_property( 'Content-Disposition', """Set the Content-Disposition header using the given filename. The value will be used for the ``filename`` directive. For example, given ``'report.pdf'``, the Content-Disposition header would be set to: ``'attachment; filename="report.pdf"'``. As per `RFC 6266 <https://tools.ietf.org/html/rfc6266#appendix-D>`_ recommendations, non-ASCII filenames will be encoded using the ``filename*`` directive, whereas ``filename`` will contain the US ASCII fallback. """, format_content_disposition) etag = header_property( 'ETag', """Set the ETag header. The ETag header will be wrapped with double quotes ``"value"`` in case the user didn't pass it. """, format_etag_header) expires = header_property( 'Expires', """Set the Expires header. Set to a ``datetime`` (UTC) instance. Note: Falcon will format the ``datetime`` as an HTTP date string. """, dt_to_http) last_modified = header_property( 'Last-Modified', """Set the Last-Modified header. Set to a ``datetime`` (UTC) instance. Note: Falcon will format the ``datetime`` as an HTTP date string. """, dt_to_http) location = header_property( 'Location', """Set the Location header. This value will be URI encoded per RFC 3986. If the value that is being set is already URI encoded it should be decoded first or the header should be set manually using the set_header method. """, uri_encode) retry_after = header_property( 'Retry-After', """Set the Retry-After header. The expected value is an integral number of seconds to use as the value for the header. The HTTP-date syntax is not supported. """, str) vary = header_property( 'Vary', """Value to use for the Vary header. Set this property to an iterable of header names. For a single asterisk or field value, simply pass a single-element ``list`` or ``tuple``. The "Vary" header field in a response describes what parts of a request message, aside from the method, Host header field, and request target, might influence the origin server's process for selecting and representing this response. The value consists of either a single asterisk ("*") or a list of header field names (case-insensitive). (See also: RFC 7231, Section 7.1.4) """, format_header_value_list) accept_ranges = header_property( 'Accept-Ranges', """Set the Accept-Ranges header. The Accept-Ranges header field indicates to the client which range units are supported (e.g. "bytes") for the target resource. If range requests are not supported for the target resource, the header may be set to "none" to advise the client not to attempt any such requests. Note: "none" is the literal string, not Python's built-in ``None`` type. """) def _set_media_type(self, media_type=None): """Set a content-type; wrapper around set_header. Args: media_type: Media type to use for the Content-Type header. """ # PERF(kgriffs): Using "in" like this is faster than dict.setdefault() # in most cases, except on PyPy where it is only a fraction of a # nanosecond slower. Last tested on Python versions 3.5-3.7. if media_type is not None and 'content-type' not in self._headers: self._headers['content-type'] = media_type def _wsgi_headers(self, media_type=None): """Convert headers into the format expected by WSGI servers. Args: media_type: Default media type to use for the Content-Type header if the header was not set explicitly (default ``None``). """ headers = self._headers # PERF(vytas): uglier inline version of Response._set_media_type if media_type is not None and 'content-type' not in headers: headers['content-type'] = media_type items = list(headers.items()) if self._extra_headers: items += self._extra_headers # NOTE(kgriffs): It is important to append these after self._extra_headers # in case the latter contains Set-Cookie headers that should be # overridden by a call to unset_cookie(). if self._cookies is not None: # PERF(tbug): # The below implementation is ~23% faster than # the alternative: # # self._cookies.output().split("\\r\\n") # # Even without the .split("\\r\\n"), the below # is still ~17% faster, so don't use .output() items += [('set-cookie', c.OutputString()) for c in self._cookies.values()] return items
class Response(object): """Represents an HTTP response to a client request. Note: `Response` is not meant to be instantiated directly by responders. Keyword Arguments: options (dict): Set of global options passed from the API handler. Attributes: status (str): HTTP status line (e.g., '200 OK'). Falcon requires the full status line, not just the code (e.g., 200). This design makes the framework more efficient because it does not have to do any kind of conversion or lookup when composing the WSGI response. If not set explicitly, the status defaults to '200 OK'. Note: Falcon provides a number of constants for common status codes. They all start with the ``HTTP_`` prefix, as in: ``falcon.HTTP_204``. body (str or unicode): String representing response content. If Unicode, Falcon will encode as UTF-8 in the response. If data is already a byte string, use the data attribute instead (it's faster). data (bytes): Byte string representing response content. Use this attribute in lieu of `body` when your content is already a byte string (``str`` or ``bytes`` in Python 2, or simply ``bytes`` in Python 3). See also the note below. Note: Under Python 2.x, if your content is of type ``str``, using the `data` attribute instead of `body` is the most efficient approach. However, if your text is of type ``unicode``, you will need to use the `body` attribute instead. Under Python 3.x, on the other hand, the 2.x ``str`` type can be thought of as having been replaced by what was once the ``unicode`` type, and so you will need to always use the `body` attribute for strings to ensure Unicode characters are properly encoded in the HTTP response. media (object): A serializable object supported by the media handlers configured via :class:`falcon.RequestOptions`. See :ref:`media` for more information regarding media handling. stream: Either a file-like object with a `read()` method that takes an optional size argument and returns a block of bytes, or an iterable object, representing response content, and yielding blocks as byte strings. Falcon will use *wsgi.file_wrapper*, if provided by the WSGI server, in order to efficiently serve file-like objects. stream_len (int): Expected length of `stream`. If `stream` is set, but `stream_len` is not, Falcon will not supply a Content-Length header to the WSGI server. Consequently, the server may choose to use chunked encoding or one of the other strategies suggested by PEP-3333. context (dict): Dictionary to hold any data about the response which is specific to your app. Falcon itself will not interact with this attribute after it has been initialized. context_type (class): Class variable that determines the factory or type to use for initializing the `context` attribute. By default, the framework will instantiate standard ``dict`` objects. However, you may override this behavior by creating a custom child class of ``falcon.Response``, and then passing that new class to `falcon.API()` by way of the latter's `response_type` parameter. Note: When overriding `context_type` with a factory function (as opposed to a class), the function is called like a method of the current Response instance. Therefore the first argument is the Response instance itself (self). options (dict): Set of global options passed from the API handler. """ __slots__ = ( 'body', 'data', '_headers', '_cookies', 'status', 'stream', 'stream_len', 'context', 'options', '__dict__', ) # Child classes may override this context_type = None def __init__(self, options=None): self.status = '200 OK' self._headers = {} self.options = ResponseOptions() if options is None else options # NOTE(tbug): will be set to a SimpleCookie object # when cookie is set via set_cookie self._cookies = None self._media = None self.body = None self.data = None self.stream = None self.stream_len = None if self.context_type is None: # PERF(kgriffs): The literal syntax is more efficient than dict(). self.context = {} else: self.context = self.context_type() @property def media(self): return self._media @media.setter def media(self, obj): self._media = obj if not self.content_type: self.content_type = self.options.default_media_type handler = self.options.media_handlers.find_by_media_type( self.content_type, self.options.default_media_type ) self.data = handler.serialize(self._media) def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.status) def set_stream(self, stream, stream_len): """Convenience method for setting both `stream` and `stream_len`. Although the `stream` and `stream_len` properties may be set directly, using this method ensures `stream_len` is not accidentally neglected when the length of the stream is known in advance. Note: If the stream length is unknown, you can set `stream` directly, and ignore `stream_len`. In this case, the WSGI server may choose to use chunked encoding or one of the other strategies suggested by PEP-3333. """ self.stream = stream self.stream_len = stream_len def set_cookie(self, name, value, expires=None, max_age=None, domain=None, path=None, secure=None, http_only=True): """Set a response cookie. Note: This method can be called multiple times to add one or more cookies to the response. See Also: To learn more about setting cookies, see :ref:`Setting Cookies <setting-cookies>`. The parameters listed below correspond to those defined in `RFC 6265`_. Args: name (str): Cookie name value (str): Cookie value Keyword Args: expires (datetime): Specifies when the cookie should expire. By default, cookies expire when the user agent exits. (See also: RFC 6265, Section 4.1.2.1) max_age (int): Defines the lifetime of the cookie in seconds. By default, cookies expire when the user agent exits. If both `max_age` and `expires` are set, the latter is ignored by the user agent. Note: Coercion to ``int`` is attempted if provided with ``float`` or ``str``. (See also: RFC 6265, Section 4.1.2.2) domain (str): Restricts the cookie to a specific domain and any subdomains of that domain. By default, the user agent will return the cookie only to the origin server. When overriding this default behavior, the specified domain must include the origin server. Otherwise, the user agent will reject the cookie. (See also: RFC 6265, Section 4.1.2.3) path (str): Scopes the cookie to the given path plus any subdirectories under that path (the "/" character is interpreted as a directory separator). If the cookie does not specify a path, the user agent defaults to the path component of the requested URI. Warning: User agent interfaces do not always isolate cookies by path, and so this should not be considered an effective security measure. (See also: RFC 6265, Section 4.1.2.4) secure (bool): Direct the client to only return the cookie in subsequent requests if they are made over HTTPS (default: ``True``). This prevents attackers from reading sensitive cookie data. Note: The default value for this argument is normally ``True``, but can be modified by setting :py:attr:`~.ResponseOptions.secure_cookies_by_default` via :any:`API.resp_options`. Warning: For the `secure` cookie attribute to be effective, your application will need to enforce HTTPS. (See also: RFC 6265, Section 4.1.2.5) http_only (bool): Direct the client to only transfer the cookie with unscripted HTTP requests (default: ``True``). This is intended to mitigate some forms of cross-site scripting. (See also: RFC 6265, Section 4.1.2.6) Raises: KeyError: `name` is not a valid cookie name. ValueError: `value` is not a valid cookie value. .. _RFC 6265: http://tools.ietf.org/html/rfc6265 """ if not is_ascii_encodable(name): raise KeyError('"name" is not ascii encodable') if not is_ascii_encodable(value): raise ValueError('"value" is not ascii encodable') if PY2: name = str(name) value = str(value) if self._cookies is None: self._cookies = SimpleCookie() try: self._cookies[name] = value except CookieError as e: # pragma: no cover # NOTE(tbug): we raise a KeyError here, to avoid leaking # the CookieError to the user. SimpleCookie (well, BaseCookie) # only throws CookieError on issues with the cookie key raise KeyError(str(e)) if expires: # set Expires on cookie. Format is Wdy, DD Mon YYYY HH:MM:SS GMT # NOTE(tbug): we never actually need to # know that GMT is named GMT when formatting cookies. # It is a function call less to just write "GMT" in the fmt string: fmt = '%a, %d %b %Y %H:%M:%S GMT' if expires.tzinfo is None: # naive self._cookies[name]['expires'] = expires.strftime(fmt) else: # aware gmt_expires = expires.astimezone(GMT_TIMEZONE) self._cookies[name]['expires'] = gmt_expires.strftime(fmt) if max_age: # RFC 6265 section 5.2.2 says about the max-age value: # "If the remainder of attribute-value contains a non-DIGIT # character, ignore the cookie-av." # That is, RFC-compliant response parsers will ignore the max-age # attribute if the value contains a dot, as in floating point # numbers. Therefore, attempt to convert the value to an integer. self._cookies[name]['max-age'] = int(max_age) if domain: self._cookies[name]['domain'] = domain if path: self._cookies[name]['path'] = path if secure is None: is_secure = self.options.secure_cookies_by_default else: is_secure = secure if is_secure: self._cookies[name]['secure'] = True if http_only: self._cookies[name]['httponly'] = http_only def unset_cookie(self, name): """Unset a cookie in the response Clears the contents of the cookie, and instructs the user agent to immediately expire its own copy of the cookie. Warning: In order to successfully remove a cookie, both the path and the domain must match the values that were used when the cookie was created. """ if self._cookies is None: self._cookies = SimpleCookie() self._cookies[name] = '' # NOTE(Freezerburn): SimpleCookie apparently special cases the # expires attribute to automatically use strftime and set the # time as a delta from the current time. We use -1 here to # basically tell the browser to immediately expire the cookie, # thus removing it from future request objects. self._cookies[name]['expires'] = -1 def get_header(self, name): """Retrieve the raw string value for the given header. Args: name (str): Header name, case-insensitive. Must be of type ``str`` or ``StringType``, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. Returns: str: The header's value if set, otherwise ``None``. """ return self._headers.get(name.lower(), None) def set_header(self, name, value): """Set a header for this response to a given value. Warning: Calling this method overwrites the existing value, if any. Warning: For setting cookies, see instead :meth:`~.set_cookie` Args: name (str): Header name (case-insensitive). The restrictions noted below for the header's value also apply here. value (str): Value for the header. Must be of type ``str`` or ``StringType`` and contain only US-ASCII characters. Under Python 2.x, the ``unicode`` type is also accepted, although such strings are also limited to US-ASCII. """ if PY2: # NOTE(kgriffs): uwsgi fails with a TypeError if any header # is not a str, so do the conversion here. It's actually # faster to not do an isinstance check. str() will encode # to US-ASCII. name = str(name) value = str(value) # NOTE(kgriffs): normalize name by lowercasing it self._headers[name.lower()] = value def delete_header(self, name): """Delete a header for this response. If the header was not previously set, do nothing. Args: name (str): Header name (case-insensitive). Must be of type ``str`` or ``StringType`` and contain only US-ASCII characters. Under Python 2.x, the ``unicode`` type is also accepted, although such strings are also limited to US-ASCII. """ # NOTE(kgriffs): normalize name by lowercasing it self._headers.pop(name.lower(), None) def append_header(self, name, value): """Set or append a header for this response. Warning: If the header already exists, the new value will be appended to it, delimited by a comma. Most header specifications support this format, Set-Cookie being the notable exceptions. Warning: For setting cookies, see :py:meth:`~.set_cookie` Args: name (str): Header name (case-insensitive). The restrictions noted below for the header's value also apply here. value (str): Value for the header. Must be of type ``str`` or ``StringType`` and contain only US-ASCII characters. Under Python 2.x, the ``unicode`` type is also accepted, although such strings are also limited to US-ASCII. """ if PY2: # NOTE(kgriffs): uwsgi fails with a TypeError if any header # is not a str, so do the conversion here. It's actually # faster to not do an isinstance check. str() will encode # to US-ASCII. name = str(name) value = str(value) name = name.lower() if name in self._headers: value = self._headers[name] + ',' + value self._headers[name] = value def set_headers(self, headers): """Set several headers at once. Warning: Calling this method overwrites existing values, if any. Args: headers (dict or list): A dictionary of header names and values to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and *value* must be of type ``str`` or ``StringType`` and contain only US-ASCII characters. Under Python 2.x, the ``unicode`` type is also accepted, although such strings are also limited to US-ASCII. Note: Falcon can process a list of tuples slightly faster than a dict. Raises: ValueError: `headers` was not a ``dict`` or ``list`` of ``tuple``. """ if isinstance(headers, dict): headers = headers.items() # NOTE(kgriffs): We can't use dict.update because we have to # normalize the header names. _headers = self._headers if PY2: for name, value in headers: # NOTE(kgriffs): uwsgi fails with a TypeError if any header # is not a str, so do the conversion here. It's actually # faster to not do an isinstance check. str() will encode # to US-ASCII. name = str(name) value = str(value) _headers[name.lower()] = value else: for name, value in headers: _headers[name.lower()] = value def add_link(self, target, rel, title=None, title_star=None, anchor=None, hreflang=None, type_hint=None): """Add a link header to the response. See also: https://tools.ietf.org/html/rfc5988 Note: Calling this method repeatedly will cause each link to be appended to the Link header value, separated by commas. Note: So-called "link-extension" elements, as defined by RFC 5988, are not yet supported. See also Issue #288. Args: target (str): Target IRI for the resource identified by the link. Will be converted to a URI, if necessary, per RFC 3987, Section 3.1. rel (str): Relation type of the link, such as "next" or "bookmark". See also http://goo.gl/618GHr for a list of registered link relation types. Keyword Args: title (str): Human-readable label for the destination of the link (default ``None``). If the title includes non-ASCII characters, you will need to use `title_star` instead, or provide both a US-ASCII version using `title` and a Unicode version using `title_star`. title_star (tuple of str): Localized title describing the destination of the link (default ``None``). The value must be a two-member tuple in the form of (*language-tag*, *text*), where *language-tag* is a standard language identifier as defined in RFC 5646, Section 2.1, and *text* is a Unicode string. Note: *language-tag* may be an empty string, in which case the client will assume the language from the general context of the current request. Note: *text* will always be encoded as UTF-8. If the string contains non-ASCII characters, it should be passed as a ``unicode`` type string (requires the 'u' prefix in Python 2). anchor (str): Override the context IRI with a different URI (default None). By default, the context IRI for the link is simply the IRI of the requested resource. The value provided may be a relative URI. hreflang (str or iterable): Either a single *language-tag*, or a ``list`` or ``tuple`` of such tags to provide a hint to the client as to the language of the result of following the link. A list of tags may be given in order to indicate to the client that the target resource is available in multiple languages. type_hint(str): Provides a hint as to the media type of the result of dereferencing the link (default ``None``). As noted in RFC 5988, this is only a hint and does not override the Content-Type header returned when the link is followed. """ # PERF(kgriffs): Heuristic to detect possiblity of an extension # relation type, in which case it will be a URL that may contain # reserved characters. Otherwise, don't waste time running the # string through uri.encode # # Example values for rel: # # "next" # "http://example.com/ext-type" # "https://example.com/ext-type" # "alternate http://example.com/ext-type" # "http://example.com/ext-type alternate" # if '//' in rel: if ' ' in rel: rel = ('"' + ' '.join([uri_encode(r) for r in rel.split()]) + '"') else: rel = '"' + uri_encode(rel) + '"' value = '<' + uri_encode(target) + '>; rel=' + rel if title is not None: value += '; title="' + title + '"' if title_star is not None: value += ("; title*=UTF-8'" + title_star[0] + "'" + uri_encode_value(title_star[1])) if type_hint is not None: value += '; type="' + type_hint + '"' if hreflang is not None: if isinstance(hreflang, STRING_TYPES): value += '; hreflang=' + hreflang else: value += '; ' value += '; '.join(['hreflang=' + lang for lang in hreflang]) if anchor is not None: value += '; anchor="' + uri_encode(anchor) + '"' if PY2: # NOTE(kgriffs): uwsgi fails with a TypeError if any header # is not a str, so do the conversion here. It's actually # faster to not do an isinstance check. str() will encode # to US-ASCII. value = str(value) _headers = self._headers if 'link' in _headers: _headers['link'] += ', ' + value else: _headers['link'] = value cache_control = header_property( 'Cache-Control', """Set the Cache-Control header. Used to set a list of cache directives to use as the value of the Cache-Control header. The list will be joined with ", " to produce the value for the header. """, format_header_value_list) content_location = header_property( 'Content-Location', """Set the Content-Location header. This value will be URI encoded per RFC 3986. If the value that is being set is already URI encoded it should be decoded first or the header should be set manually using the set_header method. """, uri_encode) content_range = header_property( 'Content-Range', """A tuple to use in constructing a value for the Content-Range header. The tuple has the form (*start*, *end*, *length*, [*unit*]), where *start* and *end* designate the range (inclusive), and *length* is the total length, or '\*' if unknown. You may pass ``int``'s for these numbers (no need to convert to ``str`` beforehand). The optional value *unit* describes the range unit and defaults to 'bytes' Note: You only need to use the alternate form, 'bytes \*/1234', for responses that use the status '416 Range Not Satisfiable'. In this case, raising ``falcon.HTTPRangeNotSatisfiable`` will do the right thing. See also: http://goo.gl/Iglhp """, format_range) content_type = header_property( 'Content-Type', """Sets the Content-Type header. Note: You can use the following predefined content types: ``falcon.MEDIA_JSON``, ``falcon.MEDIA_HTML``, ``falcon.MEDIA_JS``, ``falcon.MEDIA_XML``, ``falcon.MEDIA_TEXT``, ``falcon.MEDIA_JPEG``, ``falcon.MEDIA_PNG``, ``falcon.MEDIA_YAML`` and ``MEDIA_MSGPACK`` """) etag = header_property( 'ETag', 'Set the ETag header.') last_modified = header_property( 'Last-Modified', """Set the Last-Modified header. Set to a ``datetime`` (UTC) instance. Note: Falcon will format the ``datetime`` as an HTTP date string. """, dt_to_http) location = header_property( 'Location', """Set the Location header. This value will be URI encoded per RFC 3986. If the value that is being set is already URI encoded it should be decoded first or the header should be set manually using the set_header method. """, uri_encode) retry_after = header_property( 'Retry-After', """Set the Retry-After header. The expected value is an integral number of seconds to use as the value for the header. The HTTP-date syntax is not supported. """, str) vary = header_property( 'Vary', """Value to use for the Vary header. Set this property to an iterable of header names. For a single asterisk or field value, simply pass a single-element ``list`` or ``tuple``. "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." (Wikipedia) See also: http://goo.gl/NGHdL """, format_header_value_list) accept_ranges = header_property( 'Accept-Ranges', """Set the Accept-Ranges header. The Accept-Ranges header field indicates to the client which range units are supported (e.g. "bytes") for the target resource. If range requests are not supported for the target resource, the header may be set to "none" to advise the client not to attempt any such requests. Note: "none" is the literal string, not Python's built-in ``None`` type. """) def _set_media_type(self, media_type=None): """Wrapper around set_header to set a content-type. Args: media_type: Media type to use for the Content-Type header. """ # PERF(kgriffs): Using "in" like this is faster than using # dict.setdefault (tested on py27). set_content_type = (media_type is not None and 'content-type' not in self._headers) if set_content_type: self.set_header('content-type', media_type) def _wsgi_headers(self, media_type=None, py2=PY2): """Convert headers into the format expected by WSGI servers. Args: media_type: Default media type to use for the Content-Type header if the header was not set explicitly (default ``None``). """ headers = self._headers self._set_media_type(media_type) if py2: # PERF(kgriffs): Don't create an extra list object if # it isn't needed. items = headers.items() else: items = list(headers.items()) if self._cookies is not None: # PERF(tbug): # The below implementation is ~23% faster than # the alternative: # # self._cookies.output().split("\\r\\n") # # Even without the .split("\\r\\n"), the below # is still ~17% faster, so don't use .output() items += [('set-cookie', c.OutputString()) for c in self._cookies.values()] return items
class Response(object): """Represents an HTTP response to a client request Attributes: status: HTTP status code, such as "200 OK" (see also falcon.HTTP_*) body: String representing response content. If Unicode, Falcon will encode as UTF-8 in the response. If data is already a byte string, use the data attribute instead (it's faster). data: Byte string representing response content. stream: Iterable stream-like object, representing response content. stream_len: Expected length of stream (e.g., file size). """ __slots__ = ( '_body', # Stuff '_body_encoded', # Stuff 'data', '_headers', 'status', 'stream', 'stream_len') def __init__(self): """Initialize response attributes to default values Args: wsgierrors: File-like stream for logging errors """ self.status = '200 OK' self._headers = {} self._body = None self._body_encoded = None self.data = None self.stream = None self.stream_len = None def _get_body(self): """Returns the body as-is.""" return self._body def _set_body(self, value): """Sets the body and clears the encoded cache.""" self._body = value self._body_encoded = None # NOTE(flaper87): Lets use a property # for the body in case its content was # encoded and then modified. body = property(_get_body, _set_body) @property def body_encoded(self): """Encode the body and return it This property will encode `_body` and cache the result in the `_body_encoded` attribute. """ # NOTE(flaper87): Notice this property # is not thread-safe. If body is modified # before this property returns, we might # end up returning None. body = self._body if body and self._body_encoded is None: # NOTE(flaper87): Assume it is an # encoded str, then check and encode # if it isn't. self._body_encoded = body if isinstance(body, six.text_type): self._body_encoded = body.encode('utf-8') return self._body_encoded def set_header(self, name, value): """Set a header for this response to a given value. Warning: Overwrites the existing value, if any. Args: name: Header name to set. Must be of type str or StringType, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. value: Value for the header. Must be of type str or StringType, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. """ self._headers[name] = value def set_headers(self, headers): """Set several headers at once. May be faster than set_header(). Warning: Overwrites existing values, if any. Args: headers: A dict containing header names and values to set. Both names and values must be of type str or StringType, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. Raises: ValueError: headers was not a dictionary or list of tuples. """ self._headers.update(headers) cache_control = header_property( 'Cache-Control', """Sets the Cache-Control header. Used to set a list of cache directives to use as the value of the Cache-Control header. The list will be joined with ", " to produce the value for the header. """, lambda v: ', '.join(v)) content_location = header_property('Content-Location', 'Sets the Content-Location header.', percent_escape) content_range = header_property( 'Content-Range', """A tuple to use in constructing a value for the Content-Range header. The tuple has the form (start, end, length), where start and end is the inclusive byte range, and length is the total number of bytes, or '*' if unknown. Note: You only need to use the alternate form, "bytes */1234", for responses that use the status "416 Range Not Satisfiable". In this case, raising falcon.HTTPRangeNotSatisfiable will do the right thing. See also: http://goo.gl/Iglhp) """, format_range) content_type = header_property('Content-Type', 'Sets the Content-Type header.') etag = header_property('ETag', 'Sets the ETag header.') last_modified = header_property( 'Last-Modified', """Sets the Last-Modified header. Set to a datetime (UTC) instance. Note: Falcon will format the datetime as an HTTP date. """, dt_to_http) location = header_property('Location', 'Sets the Location header.', percent_escape) retry_after = header_property( 'Retry-After', """Sets the Retry-After header. The expected value is an integral number of seconds to use as the value for the header. The HTTP-date syntax is not supported. """, str) vary = header_property( 'Vary', """Value to use for the Vary header. From Wikipedia: "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." See also: http://goo.gl/NGHdL Set this property to an iterable of header names. For a single asterisk or field value, simply pass a single-element list or tuple. """, lambda v: ', '.join(v)) def _wsgi_headers(self, media_type=None): """Convert headers into the format expected by WSGI servers. Note: URLs are percent-escaped automatically if they contain Unicode characters. Args: media_type: Default media type to use for the Content-Type header if the header was not set explicitly. (default None) """ headers = self._headers set_content_type = (media_type is not None and 'Content-Type' not in headers and 'content-type' not in headers) if set_content_type: headers['Content-Type'] = media_type return list(headers.items())
class Response(object): """Represents an HTTP response to a client request. Note: `Response` is not meant to be instantiated directly by responders. Attributes: status (str): HTTP status line (e.g., '200 OK'). Falcon requires the full status line, not just the code (e.g., 200). This design makes the framework more efficient because it does not have to do any kind of conversion or lookup when composing the WSGI response. If not set explicitly, the status defaults to '200 OK'. Note: Falcon provides a number of constants for common status codes. They all start with the ``HTTP_`` prefix, as in: ``falcon.HTTP_204``. body (str or unicode): String representing response content. If Unicode, Falcon will encode as UTF-8 in the response. If data is already a byte string, use the data attribute instead (it's faster). body_encoded (bytes): Returns a UTF-8 encoded version of `body`. data (bytes): Byte string representing response content. Use this attribute in lieu of `body` when your content is already a byte string (``str`` or ``bytes`` in Python 2, or simply ``bytes`` in Python 3). See also the note below. Note: Under Python 2.x, if your content is of type ``str``, using the `data` attribute instead of `body` is the most efficient approach. However, if your text is of type ``unicode``, you will need to use the `body` attribute instead. Under Python 3.x, on the other hand, the 2.x ``str`` type can be thought of as having been replaced by what was once the ``unicode`` type, and so you will need to always use the `body` attribute for strings to ensure Unicode characters are properly encoded in the HTTP response. stream: Either a file-like object with a `read()` method that takes an optional size argument and returns a block of bytes, or an iterable object, representing response content, and yielding blocks as byte strings. Falcon will use *wsgi.file_wrapper*, if provided by the WSGI server, in order to efficiently serve file-like objects. stream_len (int): Expected length of `stream` (e.g., file size). """ __slots__ = ( '_body', # Stuff '_body_encoded', # Stuff 'data', '_headers', 'status', 'stream', 'stream_len') def __init__(self): self.status = '200 OK' self._headers = {} self._body = None self._body_encoded = None self.data = None self.stream = None self.stream_len = None def _get_body(self): return self._body def _set_body(self, value): self._body = value self._body_encoded = None # NOTE(flaper87): Lets use a property # for the body in case its content was # encoded and then modified. body = property(_get_body, _set_body) @property def body_encoded(self): # NOTE(flaper87): Notice this property # is not thread-safe. If body is modified # before this property returns, we might # end up returning None. body = self._body if body and self._body_encoded is None: # NOTE(flaper87): Assume it is an # encoded str, then check and encode # if it isn't. self._body_encoded = body if isinstance(body, six.text_type): self._body_encoded = body.encode('utf-8') return self._body_encoded def set_stream(self, stream, stream_len): """Convenience method for setting both `stream` and `stream_len`. Although the `stream` and `stream_len` properties may be set directly, using this method ensures `stream_len` is not accidentally neglected. """ self.stream = stream self.stream_len = stream_len def set_header(self, name, value): """Set a header for this response to a given value. Warning: Calling this method overwrites the existing value, if any. Args: name (str): Header name to set (case-insensitive). Must be of type ``str`` or ``StringType``, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. value (str): Value for the header. Must be of type ``str`` or ``StringType``, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. """ # NOTE(kgriffs): normalize name by lowercasing it self._headers[name.lower()] = value def append_header(self, name, value): """Set or append a header for this response. Warning: If the header already exists, the new value will be appended to it, delimited by a comma. Most header specifications support this format, Cookie and Set-Cookie being the notable exceptions. Args: name (str): Header name to set (case-insensitive). Must be of type ``str`` or ``StringType``, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. value (str): Value for the header. Must be of type ``str`` or ``StringType``, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. """ name = name.lower() if name in self._headers: value = self._headers[name] + ',' + value self._headers[name] = value def set_headers(self, headers): """Set several headers at once. Warning: Calling this method overwrites existing values, if any. Args: headers (dict or list): A dictionary of header names and values to set, or ``list`` of (*name*, *value*) tuples. Both *name* and *value* must be of type ``str`` or ``StringType``, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. Note: Falcon can process a list of tuples slightly faster than a dict. Raises: ValueError: `headers` was not a ``dict`` or ``list`` of ``tuple``. """ if isinstance(headers, dict): headers = headers.items() # NOTE(kgriffs): We can't use dict.update because we have to # normalize the header names. _headers = self._headers for name, value in headers: _headers[name.lower()] = value def add_link(self, target, rel, title=None, title_star=None, anchor=None, hreflang=None, type_hint=None): """ Add a link header to the response. See also: https://tools.ietf.org/html/rfc5988 Note: Calling this method repeatedly will cause each link to be appended to the Link header value, separated by commas. Note: So-called "link-extension" elements, as defined by RFC 5988, are not yet supported. See also Issue #288. Args: target (str): Target IRI for the resource identified by the link. Will be converted to a URI, if necessary, per RFC 3987, Section 3.1. rel (str): Relation type of the link, such as "next" or "bookmark". See also http://goo.gl/618GHr for a list of registered link relation types. Kwargs: title (str): Human-readable label for the destination of the link (default ``None``). If the title includes non-ASCII characters, you will need to use `title_star` instead, or provide both a US-ASCII version using `title` and a Unicode version using `title_star`. title_star (tuple of str): Localized title describing the destination of the link (default ``None``). The value must be a two-member tuple in the form of (*language-tag*, *text*), where *language-tag* is a standard language identifier as defined in RFC 5646, Section 2.1, and *text* is a Unicode string. Note: *language-tag* may be an empty string, in which case the client will assume the language from the general context of the current request. Note: *text* will always be encoded as UTF-8. If the string contains non-ASCII characters, it should be passed as a ``unicode`` type string (requires the 'u' prefix in Python 2). anchor (str): Override the context IRI with a different URI (default None). By default, the context IRI for the link is simply the IRI of the requested resource. The value provided may be a relative URI. hreflang (str or iterable): Either a single *language-tag*, or a ``list`` or ``tuple`` of such tags to provide a hint to the client as to the language of the result of following the link. A list of tags may be given in order to indicate to the client that the target resource is available in multiple languages. type_hint(str): Provides a hint as to the media type of the result of dereferencing the link (default ``None``). As noted in RFC 5988, this is only a hint and does not override the Content-Type header returned when the link is followed. """ # PERF(kgriffs): Heuristic to detect possiblity of an extension # relation type, in which case it will be a URL that may contain # reserved characters. Otherwise, don't waste time running the # string through uri.encode # # Example values for rel: # # "next" # "http://example.com/ext-type" # "https://example.com/ext-type" # "alternate http://example.com/ext-type" # "http://example.com/ext-type alternate" # if '//' in rel: if ' ' in rel: rel = ('"' + ' '.join([uri.encode(r) for r in rel.split()]) + '"') else: rel = '"' + uri.encode(rel) + '"' value = '<' + uri.encode(target) + '>; rel=' + rel if title is not None: value += '; title="' + title + '"' if title_star is not None: value += ("; title*=UTF-8'" + title_star[0] + "'" + uri.encode_value(title_star[1])) if type_hint is not None: value += '; type="' + type_hint + '"' if hreflang is not None: if isinstance(hreflang, six.string_types): value += '; hreflang=' + hreflang else: value += '; ' value += '; '.join(['hreflang=' + lang for lang in hreflang]) if anchor is not None: value += '; anchor="' + uri.encode(anchor) + '"' _headers = self._headers if 'link' in _headers: _headers['link'] += ', ' + value else: _headers['link'] = value cache_control = header_property( 'Cache-Control', """Sets the Cache-Control header. Used to set a list of cache directives to use as the value of the Cache-Control header. The list will be joined with ", " to produce the value for the header. """, lambda v: ', '.join(v)) content_location = header_property('Content-Location', 'Sets the Content-Location header.', uri.encode) content_range = header_property( 'Content-Range', """A tuple to use in constructing a value for the Content-Range header. The tuple has the form (*start*, *end*, *length*), where *start* and *end* designate the byte range (inclusive), and *length* is the total number of bytes, or '*' if unknown. You may pass ``int``'s for these numbers (no need to convert to ``str`` beforehand). Note: You only need to use the alternate form, 'bytes */1234', for responses that use the status '416 Range Not Satisfiable'. In this case, raising ``falcon.HTTPRangeNotSatisfiable`` will do the right thing. See also: http://goo.gl/Iglhp """, format_range) content_type = header_property('Content-Type', 'Sets the Content-Type header.') etag = header_property('ETag', 'Sets the ETag header.') last_modified = header_property( 'Last-Modified', """Sets the Last-Modified header. Set to a ``datetime`` (UTC) instance. Note: Falcon will format the ``datetime`` as an HTTP date string. """, dt_to_http) location = header_property('Location', 'Sets the Location header.', uri.encode) retry_after = header_property( 'Retry-After', """Sets the Retry-After header. The expected value is an integral number of seconds to use as the value for the header. The HTTP-date syntax is not supported. """, str) vary = header_property( 'Vary', """Value to use for the Vary header. Set this property to an iterable of header names. For a single asterisk or field value, simply pass a single-element ``list`` or ``tuple``. "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." (Wikipedia) See also: http://goo.gl/NGHdL """, lambda v: ', '.join(v)) def _wsgi_headers(self, media_type=None): """Convert headers into the format expected by WSGI servers. Args: media_type: Default media type to use for the Content-Type header if the header was not set explicitly (default ``None``). """ headers = self._headers # PERF(kgriffs): Using "in" like this is faster than using # dict.setdefault (tested on py27). set_content_type = (media_type is not None and 'content-type' not in headers) if set_content_type: headers['content-type'] = media_type if six.PY2: # pragma: no cover # PERF(kgriffs): Don't create an extra list object if # it isn't needed. return headers.items() return list(headers.items()) # pragma: no cover
class Response(object): """Represents an HTTP response to a client request. Note: `Response` is not meant to be instantiated directly by responders. Attributes: status (str): HTTP status line, such as "200 OK" Note: Falcon provides a number of constants for common status codes. They all start with the ``HTTP_`` prefix, as in: ``falcon.HTTP_204``. body (str or unicode): String representing response content. If Unicode, Falcon will encode as UTF-8 in the response. If data is already a byte string, use the data attribute instead (it's faster). body_encoded (bytes): Returns a UTF-8 encoded version of `body`. data (bytes): Byte string representing response content. Note: Under Python 2.x, if your content is of type *str*, setting efficient. However, if your text is of type *unicode*, you will want to use the *body* attribute instead. Under Python 3.x, the 2.x *str* type can be thought of as having been replaced with what was once the *unicode* type, and so you will want to use the `body` attribute to ensure Unicode characters are properly encoded in the response body. stream: Either a file-like object with a *read()* method that takes an optional size argument and returns a block of bytes, or an iterable object, representing response content, and yielding blocks as byte strings. Falcon will use wsgi.file_wrapper, if provided by the WSGI server, in order to efficiently serve file-like objects. stream_len (int): Expected length of *stream* (e.g., file size). """ __slots__ = ( '_body', # Stuff '_body_encoded', # Stuff 'data', '_headers', 'status', 'stream', 'stream_len') def __init__(self): self.status = '200 OK' self._headers = {} self._body = None self._body_encoded = None self.data = None self.stream = None self.stream_len = None def _get_body(self): return self._body def _set_body(self, value): self._body = value self._body_encoded = None # NOTE(flaper87): Lets use a property # for the body in case its content was # encoded and then modified. body = property(_get_body, _set_body) @property def body_encoded(self): # NOTE(flaper87): Notice this property # is not thread-safe. If body is modified # before this property returns, we might # end up returning None. body = self._body if body and self._body_encoded is None: # NOTE(flaper87): Assume it is an # encoded str, then check and encode # if it isn't. self._body_encoded = body if isinstance(body, six.text_type): self._body_encoded = body.encode('utf-8') return self._body_encoded def set_header(self, name, value): """Set a header for this response to a given value. Warning: Calling this method overwrites the existing value, if any. Args: name (str): Header name to set (case-insensitive). Must be of type str or StringType, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. value (str): Value for the header. Must be of type str or StringType, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. """ # NOTE(kgriffs): normalize name by lowercasing it self._headers[name.lower()] = value def set_headers(self, headers): """Set several headers at once. Warning: Calling this method overwrites existing values, if any. Args: headers (dict or list): A dictionary of header names and values to set, or list of (name, value) tuples. Both names and values must be of type str or StringType, and only character values 0x00 through 0xFF may be used on platforms that use wide characters. Note: Falcon can process a list of tuples slightly faster than a dict. Raises: ValueError: headers was not a dictionary or list of tuples. """ if isinstance(headers, dict): headers = headers.items() # NOTE(kgriffs): We can't use dict.update because we have to # normalize the header names. _headers = self._headers for name, value in headers: _headers[name.lower()] = value cache_control = header_property( 'Cache-Control', """Sets the Cache-Control header. Used to set a list of cache directives to use as the value of the Cache-Control header. The list will be joined with ", " to produce the value for the header. """, lambda v: ', '.join(v)) content_location = header_property('Content-Location', 'Sets the Content-Location header.', uri.encode) content_range = header_property( 'Content-Range', """A tuple to use in constructing a value for the Content-Range header. The tuple has the form ``(start, end, length)``, where *start* and *end* designate the byte range (inclusive), and *length* is the total number of bytes, or '*' if unknown. You may use *int*'s for these numbers (no need to convert to a *str* first). Note: You only need to use the alternate form, "bytes */1234", for responses that use the status "416 Range Not Satisfiable". In this case, raising falcon.HTTPRangeNotSatisfiable will do the right thing. See also: http://goo.gl/Iglhp """, format_range) content_type = header_property('Content-Type', 'Sets the Content-Type header.') etag = header_property('ETag', 'Sets the ETag header.') last_modified = header_property( 'Last-Modified', """Sets the Last-Modified header. Set to a datetime (UTC) instance. Note: Falcon will format the datetime as an HTTP date. """, dt_to_http) location = header_property('Location', 'Sets the Location header.', uri.encode) retry_after = header_property( 'Retry-After', """Sets the Retry-After header. The expected value is an integral number of seconds to use as the value for the header. The HTTP-date syntax is not supported. """, str) vary = header_property( 'Vary', """Value to use for the Vary header. Set this property to an iterable of header names. For a single asterisk or field value, simply pass a single-element list or tuple. "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." (Wikipedia) See also: http://goo.gl/NGHdL """, lambda v: ', '.join(v)) def _wsgi_headers(self, media_type=None): """Convert headers into the format expected by WSGI servers. Args: media_type: Default media type to use for the Content-Type header if the header was not set explicitly (default None). """ headers = self._headers # PERF(kgriffs): Using "in" like this is faster than using # dict.setdefault (tested on py27). set_content_type = (media_type is not None and 'content-type' not in headers) if set_content_type: headers['content-type'] = media_type if six.PY2: # pragma: no cover # PERF(kgriffs): Don't create an extra list object if # it isn't needed. return headers.items() return list(headers.items()) # pragma: no cover
def set_headers(self, headers): content_location = header_property('Content-Location', 'Sets the Content-Location header.', uri.encode)