Merge remote-tracking branch 'upstream/master' into testing_refactor
Conflicts: tests/test_headers.py
This commit is contained in:
@@ -55,7 +55,7 @@ class API(object):
|
||||
"""
|
||||
|
||||
req = Request(env)
|
||||
resp = Response()
|
||||
resp = Response(self._media_type)
|
||||
|
||||
responder, params = self._get_responder(req.path, req.method)
|
||||
|
||||
@@ -75,9 +75,12 @@ class API(object):
|
||||
#
|
||||
use_body = not should_ignore_body(resp.status, req.method)
|
||||
if use_body:
|
||||
set_content_length(resp)
|
||||
content_length = set_content_length(resp)
|
||||
else:
|
||||
content_length = 0
|
||||
|
||||
start_response(resp.status, resp._wsgi_headers(self._media_type))
|
||||
set_content_type = (content_length != 0)
|
||||
start_response(resp.status, resp._wsgi_headers(set_content_type))
|
||||
|
||||
# Return an iterable for the body, per the WSGI spec
|
||||
if use_body:
|
||||
|
||||
@@ -36,8 +36,8 @@ IGNORE_BODY_STATUS_CODES = set([
|
||||
'204 No Content',
|
||||
'304 Not Modified',
|
||||
'100 Continue',
|
||||
'101 Switching Protocols',
|
||||
'102 Processing'])
|
||||
'101 Switching Protocols'
|
||||
])
|
||||
|
||||
|
||||
def should_ignore_body(status, method):
|
||||
@@ -67,21 +67,23 @@ def set_content_length(resp):
|
||||
resp: The response object on which to set the content length.
|
||||
|
||||
"""
|
||||
content_length = 0
|
||||
|
||||
if resp.body is not None:
|
||||
# Since body is assumed to be a byte string (str in Python 2, bytes in
|
||||
# Python 3), figure out the length using standard functions.
|
||||
resp.set_header('Content-Length', str(len(resp.body)))
|
||||
content_length = len(resp.body)
|
||||
elif resp.stream is not None:
|
||||
if resp.stream_len is not None:
|
||||
# Total stream length is known in advance (e.g., streaming a file)
|
||||
resp.set_header('Content-Length', str(resp.stream_len))
|
||||
content_length = resp.stream_len
|
||||
else:
|
||||
# Stream given, but length is unknown (dynamically-generated body)
|
||||
pass
|
||||
else:
|
||||
# No body given
|
||||
resp.set_header('Content-Length', '0')
|
||||
# ...do not set the header.
|
||||
return -1
|
||||
|
||||
resp.set_header('Content-Length', str(content_length))
|
||||
return content_length
|
||||
|
||||
|
||||
def prepare_wsgi_content(resp):
|
||||
|
||||
@@ -192,6 +192,23 @@ class HTTPUnsupportedMediaType(HTTPError):
|
||||
description, **kwargs)
|
||||
|
||||
|
||||
class HTTPUpgradeRequired(HTTPError):
|
||||
"""426 Upgrade Required
|
||||
|
||||
Sets title to "Upgrade Required".
|
||||
|
||||
Args:
|
||||
description: Human-friendly description of the error, along with a
|
||||
helpful suggestion or two.
|
||||
|
||||
The remaining (optional) args are the same as for HTTPError.
|
||||
|
||||
"""
|
||||
def __init__(self, description, **kwargs):
|
||||
HTTPError.__init__(self, HTTP_426, 'Upgrade Required',
|
||||
description, **kwargs)
|
||||
|
||||
|
||||
class HTTPInternalServerError(HTTPError):
|
||||
"""500 Internal Server Error
|
||||
|
||||
@@ -218,8 +235,30 @@ class HTTPServiceUnavailable(HTTPError):
|
||||
"""503 Service Unavailable
|
||||
|
||||
Args:
|
||||
Same as for HTTPError, exept status is set for you.
|
||||
title: Human-friendly error title. Set to None if you wish Falcon
|
||||
to return an empty response body (all remaining args will
|
||||
be ignored except for headers.) Do this only when you don't
|
||||
wish to disclose sensitive information about why a request was
|
||||
refused, or if the status and headers are self-descriptive.
|
||||
description: Human-friendly description of the error, along with a
|
||||
helpful suggestion or two (default None).
|
||||
retry_after: Value for the Retry-After header. If a date object, will
|
||||
serialize as an HTTP date. Otherwise, a non-negative int is
|
||||
expected, representing the number of seconds to wait. See
|
||||
also: http://goo.gl/DIrWr
|
||||
headers: A dictionary of extra headers to return in the
|
||||
response to the client (default None).
|
||||
href: A URL someone can visit to find out more information
|
||||
(default None).
|
||||
href_rel: If href is given, use this value for the rel
|
||||
attribute (default 'doc').
|
||||
href_text: If href is given, use this as the friendly
|
||||
title/description for the link (defaults to "API documentation
|
||||
for this error").
|
||||
code: An internal code that customers can reference in their
|
||||
support request or to help them when searching for knowledge
|
||||
base articles related to this error.
|
||||
|
||||
"""
|
||||
def __init__(self, title, description, **kwargs):
|
||||
def __init__(self, title, description, retry_after, **kwargs):
|
||||
HTTPError.__init__(self, HTTP_503, title, description, **kwargs)
|
||||
|
||||
@@ -20,11 +20,40 @@ CONTENT_TYPE_NAMES = set(['Content-Type', 'content-type', 'CONTENT-TYPE'])
|
||||
|
||||
|
||||
class Response(object):
|
||||
"""Represents an HTTP response to a client request"""
|
||||
"""Represents an HTTP response to a client request
|
||||
|
||||
__slots__ = ('status', '_headers', 'body', 'data', 'stream', 'stream_len')
|
||||
|
||||
def __init__(self):
|
||||
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).
|
||||
content_type: Value for the Content-Type header
|
||||
etag: Value for the ETag header
|
||||
cache_control: An array of cache directives (see http://goo.gl/fILS5
|
||||
and http://goo.gl/sM9Xx for a good description.) The array will be
|
||||
joined with ', ' to produce the value for the Cache-Control
|
||||
header.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'body',
|
||||
'cache_control',
|
||||
'content_type',
|
||||
'data',
|
||||
'etag',
|
||||
'_headers',
|
||||
'status',
|
||||
'stream',
|
||||
'stream_len'
|
||||
)
|
||||
|
||||
def __init__(self, default_media_type):
|
||||
"""Initialize response attributes to default values
|
||||
|
||||
Args:
|
||||
@@ -33,13 +62,17 @@ class Response(object):
|
||||
"""
|
||||
|
||||
self.status = '200 OK'
|
||||
self._headers = []
|
||||
self._headers = [('Content-Type', default_media_type)]
|
||||
|
||||
self.body = None
|
||||
self.data = None
|
||||
self.stream = None
|
||||
self.stream_len = None
|
||||
|
||||
self.content_type = None
|
||||
self.etag = None
|
||||
self.cache_control = None
|
||||
|
||||
def set_header(self, name, value):
|
||||
"""Set a header for this response to a given value.
|
||||
|
||||
@@ -75,15 +108,24 @@ class Response(object):
|
||||
|
||||
self._headers.extend(headers.items())
|
||||
|
||||
def _wsgi_headers(self, default_media_type):
|
||||
"""Convert headers into the format expected by WSGI servers"""
|
||||
def _wsgi_headers(self, set_content_type):
|
||||
"""Convert headers into the format expected by WSGI servers
|
||||
|
||||
if (self.body is not None) or (self.stream is not None):
|
||||
headers = self._headers
|
||||
for name, value in headers:
|
||||
if name in CONTENT_TYPE_NAMES:
|
||||
break
|
||||
else:
|
||||
self._headers.append(('Content-Type', default_media_type))
|
||||
WARNING: Only call once! Not idempotent.
|
||||
|
||||
return self._headers
|
||||
"""
|
||||
|
||||
headers = self._headers
|
||||
|
||||
if not set_content_type:
|
||||
del headers[0]
|
||||
elif self.content_type is not None:
|
||||
headers.append(('Content-Type', self.content_type))
|
||||
|
||||
if self.etag is not None:
|
||||
headers.append(('ETag', self.etag))
|
||||
|
||||
if self.cache_control is not None:
|
||||
headers.append(('Cache-Control', ', '.join(self.cache_control)))
|
||||
|
||||
return headers
|
||||
|
||||
@@ -18,7 +18,6 @@ limitations under the License.
|
||||
|
||||
HTTP_100 = '100 Continue'
|
||||
HTTP_101 = '101 Switching Protocols'
|
||||
HTTP_102 = '102 Processing'
|
||||
|
||||
HTTP_200 = '200 OK'
|
||||
HTTP_201 = '201 Created'
|
||||
@@ -51,11 +50,12 @@ HTTP_409 = '409 Conflict'
|
||||
HTTP_410 = '410 Gone'
|
||||
HTTP_411 = '411 Length Required'
|
||||
HTTP_412 = '412 Precondition Failed'
|
||||
HTTP_413 = '413 Request Entity Too Large'
|
||||
HTTP_414 = '414 Request-URI Too Large'
|
||||
HTTP_413 = '413 Payload Too Large'
|
||||
HTTP_414 = '414 URI Too Long'
|
||||
HTTP_415 = '415 Unsupported Media Type'
|
||||
HTTP_416 = '416 Requested range not satisfiable'
|
||||
HTTP_416 = '416 Range Not Satisfiable'
|
||||
HTTP_417 = '417 Expectation Failed'
|
||||
HTTP_426 = '426 Upgrade Required'
|
||||
|
||||
HTTP_500 = '500 Internal Server Error'
|
||||
HTTP_501 = '501 Not Implemented'
|
||||
|
||||
@@ -4,7 +4,6 @@ from wsgiref.simple_server import make_server
|
||||
|
||||
def application(environ, start_response):
|
||||
wsgi_errors = environ['wsgi.errors']
|
||||
pdb.set_trace()
|
||||
|
||||
start_response("200 OK", [
|
||||
('Content-Type', 'text/plain')])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from testtools.matchers import Contains, Not
|
||||
|
||||
import falcon
|
||||
import falcon.testing as testing
|
||||
import falcon
|
||||
|
||||
|
||||
class StatusTestResource:
|
||||
@@ -32,6 +32,34 @@ class DefaultContentTypeResource:
|
||||
resp.body = self.body
|
||||
|
||||
|
||||
class HeaderHelpersResource:
|
||||
|
||||
def on_get(self, req, resp):
|
||||
resp.body = "{}"
|
||||
resp.content_type = 'x-falcon/peregrine'
|
||||
resp.cache_control = [
|
||||
'public', 'private', 'no-cache', 'no-store', 'must-revalidate',
|
||||
'proxy-revalidate', 'max-age=3600', 's-maxage=60', 'no-transform'
|
||||
]
|
||||
|
||||
resp.etag = 'fa0d1a60ef6616bb28038515c8ea4cb2'
|
||||
# resp.set_last_modified() # http://goo.gl/M9Fs9
|
||||
# resp.set_retry_after() # http://goo.gl/DIrWr
|
||||
# resp.set_vary() # http://goo.gl/wyI7d
|
||||
|
||||
# # Relative URI's are OK per http://goo.gl/DbVqR
|
||||
# resp.set_location('/things/87')
|
||||
|
||||
# bytes 0-499/10240
|
||||
# resp.set_content_range(0, 499, 10 * 1024)
|
||||
|
||||
def on_head(self, req, resp):
|
||||
# Alias of set_media_type
|
||||
resp.content_type = 'x-falcon/peregrine'
|
||||
|
||||
resp.cache_control = ['no-store']
|
||||
|
||||
|
||||
class TestHeaders(testing.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
@@ -78,8 +106,8 @@ class TestHeaders(testing.TestSuite):
|
||||
host = self.resource.req.get_header('host')
|
||||
self.assertEquals(host, 'localhost:8000')
|
||||
|
||||
def test_no_body_on_1xx(self):
|
||||
self.resource = StatusTestResource(falcon.HTTP_102)
|
||||
def test_no_body_on_100(self):
|
||||
self.resource = StatusTestResource(falcon.HTTP_100)
|
||||
self.api.add_route('/1xx', self.resource)
|
||||
|
||||
body = self._simulate_request('/1xx')
|
||||
@@ -154,6 +182,33 @@ class TestHeaders(testing.TestSuite):
|
||||
content_type = 'application/atom+xml'
|
||||
self.assertIn(('Content-Type', content_type), self.srmock.headers)
|
||||
|
||||
def test_response_header_helpers_on_get(self):
|
||||
self.resource = HeaderHelpersResource()
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
self._simulate_request(self.test_route)
|
||||
|
||||
content_type = 'x-falcon/peregrine'
|
||||
self.assertIn(('Content-Type', content_type), self.srmock.headers)
|
||||
|
||||
cache_control = ('public, private, no-cache, no-store, '
|
||||
'must-revalidate, proxy-revalidate, max-age=3600, '
|
||||
's-maxage=60, no-transform')
|
||||
|
||||
self.assertIn(('Cache-Control', cache_control), self.srmock.headers)
|
||||
|
||||
etag = 'fa0d1a60ef6616bb28038515c8ea4cb2'
|
||||
self.assertIn(('ETag', etag), self.srmock.headers)
|
||||
|
||||
def test_response_header_helpers_on_head(self):
|
||||
self.resource = HeaderHelpersResource()
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
self._simulate_request(self.test_route, method="HEAD")
|
||||
|
||||
content_type = 'x-falcon/peregrine'
|
||||
self.assertNotIn(('Content-Type', content_type), self.srmock.headers)
|
||||
|
||||
self.assertIn(('Cache-Control', 'no-store'), self.srmock.headers)
|
||||
|
||||
def test_no_content_type(self):
|
||||
self.resource = DefaultContentTypeResource()
|
||||
self.api.add_route(self.test_route, self.resource)
|
||||
|
||||
@@ -12,9 +12,9 @@ class TestReqVars(testing.TestSuite):
|
||||
}
|
||||
|
||||
self.req = Request(testing.create_environ(script='/test',
|
||||
path='/hello',
|
||||
query_string=qs,
|
||||
headers=headers))
|
||||
path='/hello',
|
||||
query_string=qs,
|
||||
headers=headers))
|
||||
|
||||
def test_reconstruct_url(self):
|
||||
req = self.req
|
||||
|
||||
Reference in New Issue
Block a user