WebOb Reference — WebOb 1.8.8 documentation

# WebOb Reference

## Introduction

This document covers all the details of the Request and Response objects. It is written to be testable with `doctest` -- this affects the flavor of the documentation, perhaps to its detriment. But it also means you can feel confident that the documentation is correct.

Note

All of the code samples below are for Python 3, and will not function as-is on Python 2.

This is a somewhat different approach to reference documentation compared to the extracted documentation for the `request` and `response`.

## Request

The primary object in WebOb is `webob.Request`, a wrapper around a WSGI environment.

The basic way you create a request object is simple enough:

    >>> from webob import Request
    >>> environ = {'wsgi.url_scheme': 'http', ...}
    >>> req = Request(environ)

(Note that the WSGI environment is a dictionary with a dozen required keys, so it's a bit lengthly to show a complete example of what it would look like -- usually your WSGI server will create it.)

The request object wraps the environment; it has very little internal state of its own. Instead attributes you access read and write to the environment dictionary.

You don't have to understand the details of WSGI to use this library; this library handles those details for you. You also don't have to use this exclusively of other libraries. If those other libraries also keep their state in the environment, multiple wrappers can coexist. Examples of libraries that can coexist include paste.wsgiwrappers.Request (used by Pylons) and yaro.Request.

The WSGI environment has a number of required variables. To make it easier to test and play around with, the `Request` class has a constructor that will fill in a minimal environment:

    >>> req = Request.blank('/article?id=1')
    >>> from pprint import pprint
    >>> pprint(req.environ)
    {'HTTP_HOST': 'localhost:80',
     'PATH_INFO': '/article',
     'QUERY_STRING': 'id=1',
     'REQUEST_METHOD': 'GET',
     'SCRIPT_NAME': '',
     'SERVER_NAME': 'localhost',
     'SERVER_PORT': '80',
     'SERVER_PROTOCOL': 'HTTP/1.0',
     'wsgi.errors': <...TextIOWrapper ...'<stderr>' ...>,
     'wsgi.input': <...IO object at 0x...>,
     'wsgi.multiprocess': False,
     'wsgi.multithread': False,
     'wsgi.run_once': False,
     'wsgi.url_scheme': 'http',
     'wsgi.version': (1, 0)}

### Request Body

`req.body` is a file-like object that gives the body of the request (e.g., a POST form, the body of a PUT, etc). It's kind of boring to start, but you can set it to a string and that will be turned into a file-like object. You can read the entire body with `req.body`.

    >>> hasattr(req.body_file, 'read')
    True
    >>> req.body
    b''
    >>> req.method = 'PUT'
    >>> req.body = b'test'
    >>> hasattr(req.body_file, 'read')
    True
    >>> req.body
    b'test'

### Method & URL

All the normal parts of a request are also accessible through the request object:

    >>> req.method
    'PUT'
    >>> req.scheme
    'http'
    >>> req.script_name  # The base of the URL
    ''
    >>> req.script_name = '/blog'  # make it more interesting
    >>> req.path_info  # The yet-to-be-consumed part of the URL
    '/article'
    >>> req.content_type  # Content-Type of the request body
    ''
    >>> print(req.remote_user)  # The authenticated user (there is none set)
    None
    >>> print(req.remote_addr)  # The remote IP
    None
    >>> req.host
    'localhost:80'
    >>> req.host_url
    'http://localhost'
    >>> req.application_url
    'http://localhost/blog'
    >>> req.path_url
    'http://localhost/blog/article'
    >>> req.url
    'http://localhost/blog/article?id=1'
    >>> req.path
    '/blog/article'
    >>> req.path_qs
    '/blog/article?id=1'
    >>> req.query_string
    'id=1'

You can make new URLs:

    >>> req.relative_url('archive')
    'http://localhost/blog/archive'

For parsing the URLs, it is often useful to deal with just the next path segment on PATH_INFO:

    >>> req.path_info_peek() # Doesn't change request
    'article'
    >>> req.path_info_pop()  # Does change request!
    'article'
    >>> req.script_name
    '/blog/article'
    >>> req.path_info
    ''

### Headers

All request headers are available through a dictionary-like object `req.headers`. Keys are case-insensitive.

    >>> req.headers['Content-Type'] = 'application/x-www-urlencoded'
    >>> sorted(req.headers.items())
    [('Content-Length', '4'), ('Content-Type', 'application/x-www-urlencoded'), ('Host', 'localhost:80')]
    >>> req.environ['CONTENT_TYPE']
    'application/x-www-urlencoded'

### Query & POST variables

Requests can have variables in one of two locations: the query string (`?id=1`), or in the body of the request (generally a POST form). Note that even POST requests can have a query string, so both kinds of variables can exist at the same time. Also, a variable can show up more than once, as in `?check=a&check=b`.

For these variables WebOb uses a `MultiDict`, which is basically a dictionary wrapper on a list of key/value pairs. It looks like a single-valued dictionary, but you can access all the values of a key with `.getall(key)` (which always returns a list, possibly an empty list). You also get all key/value pairs when using `.items()` and all values with `.values()`.

Some examples:

    >>> req = Request.blank('/test?check=a&check=b&name=Bob')
    >>> req.GET
    GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')])
    >>> req.GET['check']
    'b'
    >>> req.GET.getall('check')
    ['a', 'b']
    >>> list(req.GET.items())
    [('check', 'a'), ('check', 'b'), ('name', 'Bob')]

We'll have to create a request body and change the method to get `POST`. Until we do that, the variables are boring:

    >>> req.POST
    <NoVars: Not a form request>
    >>> list(req.POST.items())  # NoVars can be read like a dict, but not written
    []
    >>> req.method = 'POST'
    >>> req.body = b'name=Joe&email=joe@example.com'
    >>> req.POST
    MultiDict([('name', 'Joe'), ('email', 'joe@example.com')])
    >>> req.POST['name']
    'Joe'

Often you won't care where the variables come from. (Even if you care about the method, the location of the variables might not be important.) There is a dictionary called `req.params` that contains variables from both sources:

    >>> req.params
    NestedMultiDict([('check', 'a'), ('check', 'b'), ('name', 'Bob'), ('name', 'Joe'), ('email', 'joe@example.com')])
    >>> req.params['name']
    'Bob'
    >>> req.params.getall('name')
    ['Bob', 'Joe']
    >>> for name, value in req.params.items():
    ...     print('%s: %r' % (name, value))
    check: 'a'
    check: 'b'
    name: 'Bob'
    name: 'Joe'
    email: 'joe@example.com'

The `POST` and `GET` nomenclature is historical -- `req.GET` can be used for non-GET requests to access query parameters, and `req.POST` can also be used for PUT requests with the appropriate Content-Type.

    >>> req = Request.blank('/test?check=a&check=b&name=Bob')
    >>> req.method = 'PUT'
    >>> req.body = b'var1=value1&var2=value2&rep=1&rep=2'
    >>> req.environ['CONTENT_LENGTH'] = str(len(req.body))
    >>> req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
    >>> req.GET
    GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')])
    >>> req.POST
    MultiDict([('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')])

#### Unicode Variables

Submissions are by default UTF-8, you can force a different character set by setting the charset on the `Request` object explicitly. A client can indicate the character set with `Content-Type: application/x-www-form-urlencoded; charset=utf8`, but very few clients actually do this (sometimes XMLHttpRequest requests will do this, as JSON is always UTF8 even when a page is served with a different character set). You can force a charset, which will affect all the variables:

    >>> req.charset = 'utf8'
    >>> req.GET
    GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')])

### Cookies

Cookies are presented in a simple dictionary. Like other variables, they will be decoded into Unicode strings if you set the charset.

    >>> req.headers['Cookie'] = 'test=value'
    >>> req.cookies
    <RequestCookies (dict-like) with values {'test': 'value'}>

### Modifying the request

The headers are all modifiable, as are other environmental variables (like `req.remote_user`, which maps to `request.environ['REMOTE_USER']`).

If you want to copy the request you can use `req.copy()`; this copies the `environ` dictionary, and the request body from `environ['wsgi.input']`.

The method `req.remove_conditional_headers(remove_encoding=True)` can be used to remove headers that might result in a `304 Not Modified` response. If you are writing some intermediary it can be useful to avoid these headers. Also if `remove_encoding` is true (the default) then any `Accept-Encoding` header will be removed, which can result in gzipped responses.

### Header Getters

In addition to `req.headers`, there are attributes for most of the request headers defined by the HTTP 1.1 specification. These attributes often return parsed forms of the headers.

#### Accept-* headers

There are several request headers that tell the server what the client accepts. These are `accept` (the Content-Type that is accepted), `accept_charset` (the charset accepted), `accept_encoding` (the Content-Encoding, like gzip, that is accepted), and `accept_language` (generally the preferred language of the client).

The objects returned support containment to test for acceptability. E.g.:

    >>> 'text/html' in req.accept
    True

Because no header means anything is potentially acceptable, this is returning True. We can set it to see more interesting behavior (the example means that `text/html` is okay, but `application/xhtml+xml` is preferred):

    >>> req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1'
    >>> req.accept
    <AcceptValidHeader ('text/html;q=0.5, application/xhtml+xml')>
    >>> 'text/html' in req.accept
    True

There are a few methods for different strategies of finding a match.

    >>> req.accept.best_match(['text/html', 'application/xhtml+xml'])
    'application/xhtml+xml'

If we just want to know everything the client prefers, in the order it is preferred:

    >>> list(req.accept)
    ['application/xhtml+xml', 'text/html']

For languages you'll often have a "fallback" language. E.g., if there's nothing better then use `en-US` (and if `en-US` is okay, ignore any less preferrable languages):

    >>> req.accept_language = 'es, pt-BR'
    >>> req.accept_language.best_match(['en-GB', 'en-US'], default_match='en-US')
    'en-US'
    >>> req.accept_language.best_match(['es', 'en-US'], default_match='en-US')
    'es'

Your fallback language must appear both in the `offers` and as the `default_match` to insure that it is returned as a best match if the client specified a preference for it.

    >>> req.accept_language = 'en-US;q=0.5, en-GB;q=0.2'
    >>> req.accept_language.best_match(['en-GB'], default_match='en-US')
    'en-GB'
    >>> req.accept_language.best_match(['en-GB', 'en-US'], default_match='en-US')
    'en-US'

#### Conditional Requests

There a number of ways to make a conditional request. A conditional request is made when the client has a document, but it is not sure if the document is up to date. If it is not, it wants a new version. If the document is up to date then it doesn't want to waste the bandwidth, and expects a `304 Not Modified` response.

ETags are generally the best technique for these kinds of requests; this is an opaque string that indicates the identity of the object. For instance, it's common to use the mtime (last modified) of the file, plus the number of bytes, and maybe a hash of the filename (if there's a possibility that the same URL could point to two different server-side filenames based on other variables). To test if a 304 response is appropriate, you can use:

    >>> server_token = 'opaque-token'
    >>> server_token in req.if_none_match # You shouldn't return 304
    False
    >>> req.if_none_match = server_token
    >>> req.if_none_match
    <ETag opaque-token>
    >>> server_token in req.if_none_match # You *should* return 304
    True

For date-based comparisons If-Modified-Since is used:

    >>> from webob import UTC
    >>> from datetime import datetime
    >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
    >>> req.headers['If-Modified-Since']
    'Sun, 01 Jan 2006 12:00:00 GMT'
    >>> server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC)
    >>> req.if_modified_since and req.if_modified_since >= server_modified
    True

For range requests there are two important headers, If-Range (which is form of conditional request) and Range (which requests a range). If the If-Range header fails to match then the full response (not a range) should be returned:

    >>> req.if_range
    IfRange(<ETag *>)
    >>> req.if_range = 'opaque-etag'
    >>> from webob import Response
    >>> res = Response(etag='opaque-etag')
    >>> res in req.if_range
    True

To get the range information:

    >>> req.range = 'bytes=0-100'
    >>> req.range
    <Range bytes 0-101>
    >>> cr = req.range.content_range(length=1000)
    >>> cr.start, cr.stop, cr.length
    (0, 101, 1000)

Note that the range headers use inclusive ranges (the last byte indexed is included), where Python always uses a range where the last index is excluded from the range. The `.stop` index is in the Python form.

Another kind of conditional request is a request (typically PUT) that includes If-Match or If-Unmodified-Since. In this case you are saying "here is an update to a resource, but don't apply it if someone else has done something since I last got the resource". If-Match means "do this if the current ETag matches the ETag I'm giving". If-Unmodified-Since means "do this if the resource has remained unchanged".

    >>> server_token in req.if_match # No If-Match means everything is ok
    True
    >>> req.if_match = server_token
    >>> server_token in req.if_match # Still OK
    True
    >>> req.if_match = 'other-token'
    >>> # Not OK, should return 412 Precondition Failed:
    >>> server_token in req.if_match
    False

For more on this kind of conditional request, see Detecting the Lost Update Problem Using Unreserved Checkout.

### Calling WSGI Applications

The request object can be used to make handy subrequests or test requests against WSGI applications. If you want to make subrequests, you should copy the request (with `req.copy()`) before sending it to multiple applications, since applications might modify the request when they are run.

There's two forms of the subrequest. The more primitive form is this:

    >>> req = Request.blank('/')
    >>> def wsgi_app(environ, start_response):
    ...     start_response('200 OK', [('Content-type', 'text/plain')])
    ...     return ['Hi!']
    >>> req.call_application(wsgi_app)
    ('200 OK', [('Content-type', 'text/plain')], ['Hi!'])

Note it returns `(status_string, header_list, app_iter)`. If `app_iter.close()` exists, it is your responsibility to call it.

A handier response can be had with:

    >>> res = req.get_response(wsgi_app)
    >>> res
    <Response ... 200 OK>
    >>> res.status
    '200 OK'
    >>> res.headers
    ResponseHeaders([('Content-type', 'text/plain')])
    >>> res.body
    'Hi!'

You can learn more about this response object in the Response section.

### Ad-Hoc Attributes

You can assign attributes to your request objects. They will all go in `environ['webob.adhoc_attrs']` (a dictionary).

    >>> req = Request.blank('/')
    >>> req.some_attr = 'blah blah blah'
    >>> new_req = Request(req.environ)
    >>> new_req.some_attr
    'blah blah blah'
    >>> req.environ['webob.adhoc_attrs']
    {'some_attr': 'blah blah blah'}

## Response

The `webob.Response` object contains everything necessary to make a WSGI response. Instances of it are in fact WSGI applications, but it can also represent the result of calling a WSGI application (as noted in Calling WSGI Applications). It can also be a way of accumulating a response in your WSGI application.

A WSGI response is made up of a status (like `200 OK`), a list of headers, and a body (or iterator that will produce a body).

### Core Attributes

The core attributes are unsurprising:

    >>> from webob import Response
    >>> res = Response()
    >>> res.status
    '200 OK'
    >>> res.headerlist
    [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '0')]
    >>> res.body
    b''

You can set any of these attributes, e.g.:

    >>> res.status = 404
    >>> res.status
    '404 Not Found'
    >>> res.status_code
    404
    >>> res.headerlist = [('Content-Type', 'text/html')]
    >>> res.body = b'test'
    >>> print(res)
    404 Not Found
    Content-Type: text/html
    Content-Length: 4

    test
    >>> res.body = "test"
    Traceback (most recent call last):
        ...
    TypeError: You cannot set Response.body to a text object (use Response.text)
    >>> res.text = "test"
    >>> res.body
    b'test'

You can set any attribute with the constructor, like `Response(charset='UTF-8')`

### Headers

In addition to `res.headerlist`, there is dictionary-like view on the list in `res.headers`:

    >>> res.headers
    ResponseHeaders([('Content-Type', 'text/html'), ('Content-Length', '4')])

This is case-insensitive. It can support multiple values for a key, though only if you use `res.headers.add(key, value)` or read them with `res.headers.getall(key)`.

### Body & app_iter

The `res.body` attribute represents the entire body of the request as a single string (not unicode, though you can set it to unicode if you have a charset defined). There is also a `res.app_iter` attribute that reprsents the body as an iterator. WSGI applications return these `app_iter` iterators instead of strings, and sometimes it can be problematic to load the entire iterator at once (for instance, if it returns the contents of a very large file). Generally it is not a problem, and often the iterator is something simple like a one-item list containing a string with the entire body.

If you set the body then Content-Length will also be set, and an `res.app_iter` will be created for you. If you set `res.app_iter` then Content-Length will be cleared, but it won't be set for you.

There is also a file-like object you can access, which will update the app_iter in-place (turning the app_iter into a list if necessary):

    >>> res = Response(content_type='text/plain', charset=None)
    >>> f = res.body_file
    >>> f.write('hey')
    Traceback (most recent call last):
        ...
    TypeError: You can only write text to Response if charset has been set
    >>> f.encoding
    >>> res.charset = 'UTF-8'
    >>> f.encoding
    'UTF-8'
    >>> f.write('test')
    >>> res.app_iter
    [b'', b'test']
    >>> res.body
    b'test'

### Header Getters

Like Request, HTTP response headers are also available as individual properties. These represent parsed forms of the headers.

Content-Type is a special case, as the type and the charset are handled through two separate properties:

    >>> res = Response()
    >>> res.content_type = 'text/html'
    >>> res.charset = 'utf8'
    >>> res.content_type
    'text/html'
    >>> res.headers['content-type']
    'text/html; charset=utf8'
    >>> res.content_type = 'application/atom+xml'
    >>> res.content_type_params
    {'charset': 'UTF-8'}
    >>> res.content_type_params = {'type': 'entry', 'charset': 'UTF-8'}
    >>> res.headers['content-type']
    'application/atom+xml; charset=UTF-8; type=entry'

Other headers:

    >>> # Used with a redirect:
    >>> res.location = 'http://localhost/foo'

    >>> # Indicates that the server accepts Range requests:
    >>> res.accept_ranges = 'bytes'

    >>> # Used by caching proxies to tell the client how old the
    >>> # response is:
    >>> res.age = 120

    >>> # Show what methods the client can do; typically used in
    >>> # a 405 Method Not Allowed response:
    >>> res.allow = ['GET', 'PUT']

    >>> # Set the cache-control header:
    >>> res.cache_control.max_age = 360
    >>> res.cache_control.no_transform = True

    >>> # Tell the browser to treat the response as an attachment:
    >>> res.content_disposition = 'attachment; filename=foo.xml'

    >>> # Used if you had gzipped the body:
    >>> res.content_encoding = 'gzip'

    >>> # What language(s) are in the content:
    >>> res.content_language = ['en']

    >>> # Seldom used header that tells the client where the content
    >>> # is from:
    >>> res.content_location = 'http://localhost/foo'

    >>> # Seldom used header that gives a hash of the body:
    >>> res.content_md5 = 'big-hash'

    >>> # Means we are serving bytes 0-500 inclusive, out of 1000 bytes total:
    >>> # you can also use the range setter shown earlier
    >>> res.content_range = (0, 501, 1000)

    >>> # The length of the content; set automatically if you set
    >>> # res.body:
    >>> res.content_length = 4

    >>> # Used to indicate the current date as the server understands
    >>> # it:
    >>> res.date = datetime.now()

    >>> # The etag:
    >>> res.etag = 'opaque-token'
    >>> # You can generate it from the body too:
    >>> res.md5_etag()
    >>> res.etag
    '1B2M2Y8AsgTpgAmY7PhCfg'

    >>> # When this page should expire from a cache (Cache-Control
    >>> # often works better):
    >>> import time
    >>> res.expires = time.time() + 60*60 # 1 hour

    >>> # When this was last modified, of course:
    >>> res.last_modified = datetime(2007, 1, 1, 12, 0, tzinfo=UTC)

    >>> # Used with 503 Service Unavailable to hint the client when to
    >>> # try again:
    >>> res.retry_after = 160

    >>> # Indicate the server software:
    >>> res.server = 'WebOb/1.0'

    >>> # Give a list of headers that the cache should vary on:
    >>> res.vary = ['Cookie']

Note in each case you can general set the header to a string to avoid any parsing, and set it to None to remove the header (or do something like `del res.vary`).

In the case of date-related headers you can set the value to a `datetime` instance (ideally with a UTC timezone), a time tuple, an integer timestamp, or a properly-formatted string.

After setting all these headers, here's the result:

    >>> for name, value in res.headerlist:
    ...     print('%s: %s' % (name, value))
    Content-Type: application/atom+xml; charset=UTF-8; type=entry
    Location: http://localhost/foo
    Accept-Ranges: bytes
    Age: 120
    Allow: GET, PUT
    Cache-Control: max-age=360, no-transform
    Content-Disposition: attachment; filename=foo.xml
    Content-Encoding: gzip
    Content-Language: en
    Content-Location: http://localhost/foo
    Content-MD5: big-hash
    Content-Range: bytes 0-500/1000
    Content-Length: 4
    Date: ... GMT
    ETag: ...
    Expires: ... GMT
    Last-Modified: Mon, 01 Jan 2007 12:00:00 GMT
    Retry-After: 160
    Server: WebOb/1.0
    Vary: Cookie

You can also set Cache-Control related attributes with `req.cache_expires(seconds, **attrs)`, like:

    >>> res = Response()
    >>> res.cache_expires(10)
    >>> res.headers['Cache-Control']
    'max-age=10'
    >>> res.cache_expires(0)
    >>> res.headers['Cache-Control']
    'max-age=0, must-revalidate, no-cache, no-store'
    >>> res.headers['Expires']
    '... GMT'

You can also use the `timedelta` constants defined, e.g.:

    >>> from webob import *
    >>> res = Response()
    >>> res.cache_expires(2*day+4*hour)
    >>> res.headers['Cache-Control']
    'max-age=187200'

### Cookies

Cookies (and the Set-Cookie header) are handled with a couple methods. Most importantly:

    >>> res.set_cookie('key', 'value', max_age=360, path='/',
    ...                domain='example.org', secure=True)
    >>> res.headers['Set-Cookie']
    'key=value; Domain=example.org; Max-Age=360; Path=/; expires=... GMT; secure'
    >>> # To delete a cookie previously set in the client:
    >>> res.delete_cookie('bad_cookie')
    >>> res.headers['Set-Cookie']
    'bad_cookie=; Max-Age=0; Path=/; expires=... GMT'

The only other real method of note (note that this does not delete the cookie from clients, only from the response object):

    >>> res.unset_cookie('key')
    >>> res.unset_cookie('bad_cookie')
    >>> print(res.headers.get('Set-Cookie'))
    None

### Binding a Request

You can bind a request (or request WSGI environ) to the response object. This is available through `res.request` or `res.environ`. This is currently only used in setting `res.location`, to make the location absolute if necessary.

### Response as a WSGI application

A response is a WSGI application, in that you can do:

    >>> req = Request.blank('/')
    >>> status, headers, app_iter = req.call_application(res)

A possible pattern for your application might be:

    >>> def my_app(environ, start_response):
    ...     req = Request(environ)
    ...     res = Response()
    ...     res.charset = 'UTF-8'
    ...     res.content_type = 'text/plain'
    ...     parts = []
    ...     for name, value in sorted(req.environ.items()):
    ...         parts.append('%s: %r' % (name, value))
    ...     res.text = '\n'.join(parts)
    ...     return res(environ, start_response)
    >>> req = Request.blank('/')
    >>> res = req.get_response(my_app)
    >>> print(res)
    200 OK
    Content-Type: text/plain; charset=UTF-8
    Content-Length: ...

    HTTP_HOST: 'localhost:80'
    PATH_INFO: '/'
    QUERY_STRING: ''
    REQUEST_METHOD: 'GET'
    SCRIPT_NAME: ''
    SERVER_NAME: 'localhost'
    SERVER_PORT: '80'
    SERVER_PROTOCOL: 'HTTP/1.0'
    wsgi.errors: <...>
    wsgi.input: <...IO... object at ...>
    wsgi.multiprocess: False
    wsgi.multithread: False
    wsgi.run_once: False
    wsgi.url_scheme: 'http'
    wsgi.version: (1, 0)

## Exceptions

In addition to Request and Response objects, there are a set of Python exceptions for different HTTP responses (3xx, 4xx, 5xx codes).

These provide a simple way to provide these non-200 response. A very simple body is provided.

    >>> from webob.exc import *
    >>> exc = HTTPTemporaryRedirect(location='foo')
    >>> req = Request.blank('/path/to/something')
    >>> print(str(req.get_response(exc)).strip())
    307 Temporary Redirect
    Location: http://localhost/path/to/foo
    Content-Length: 126
    Content-Type: text/plain; charset=UTF-8
    \r
    307 Temporary Redirect

    The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically.

Note that only if there's an `Accept: text/html` header in the request will an HTML response be given:

    >>> req.accept += 'text/html'
    >>> print(str(req.get_response(exc)).strip())
    307 Temporary Redirect
    Location: http://localhost/path/to/foo
    Content-Length: 270
    Content-Type: text/html; charset=UTF-8

    <html>
     <head>
      <title>307 Temporary Redirect</title>
     </head>
     <body>
      <h1>307 Temporary Redirect</h1>
      The resource has been moved to <a href="http://localhost/path/to/foo">http://localhost/path/to/foo</a>;
    you should be redirected automatically.

     </body>
    </html>

### Conditional WSGI Application

The Response object can handle your conditional responses for you, checking If-None-Match, If-Modified-Since, and Range/If-Range.

To enable this you must create the response like `Response(conditional_response=True)`, or make a subclass like:

    >>> class AppResponse(Response):
    ...     default_content_type = 'text/html'
    ...     default_conditional_response = True
    >>> res = AppResponse(body='0123456789',
    ...                   last_modified=datetime(2005, 1, 1, 12, 0, tzinfo=UTC))
    >>> req = Request.blank('/')
    >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
    >>> req.get_response(res)
    <Response ... 304 Not Modified>
    >>> del req.if_modified_since
    >>> res.etag = 'opaque-tag'
    >>> req.if_none_match = 'opaque-tag'
    >>> req.get_response(res)
    <Response ... 304 Not Modified>

    >>> req.if_none_match = '*'
    >>> 'x' in req.if_none_match
    True
    >>> req.if_none_match = req.if_none_match
    >>> 'x' in req.if_none_match
    True
    >>> req.if_none_match = None
    >>> 'x' in req.if_none_match
    False
    >>> req.if_match = None
    >>> 'x' in req.if_match
    True
    >>> req.if_match = req.if_match
    >>> 'x' in req.if_match
    True
    >>> req.headers.get('If-Match')
    '*'

    >>> del req.if_none_match

    >>> req.range = (1, 5)
    >>> result = req.get_response(res)
    >>> result.headers['content-range']
    'bytes 1-4/10'
    >>> result.body
    b'1234'