Merge pull request #176 from kgriffs/issues/155
fix(Request.client_accepts): Accept parsing doesn't handle application/*
This commit is contained in:
@@ -18,7 +18,7 @@ Falcon is a [high-performance Python framework][home] for building cloud APIs. I
|
||||
|
||||
**Fast.** Cloud APIs need to turn around requests quickly, and make efficient use of hardware. This is particularly important when serving many concurrent requests. Falcon processes requests [several times faster][bench] than other popular web frameworks.
|
||||
|
||||
**Light.** Only the essentials are included, with *six* being the only dependency outside the standard library. We work to keep the code lean, making Falcon easier to test, optimize, and deploy.
|
||||
**Light.** Only the essentials are included, with *six* and *mimeparse* being the only dependencies outside the standard library. We work to keep the code lean, making Falcon easier to test, optimize, and deploy.
|
||||
|
||||
**Flexible.** Falcon can be deployed in a variety of ways, depending on your needs. The framework speaks WSGI, and works great with [Python 2.6 and 2.7, PyPy, and Python 3.3][ci]. There's no tight coupling with any async framework, leaving you free to mix-and-match what you need.
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ many concurrent requests. Falcon processes requests `several times
|
||||
faster <http://falconframework.org/#Metrics>`__ than other popular web
|
||||
frameworks.
|
||||
|
||||
**Light.** Only the essentials are included, with *six* being the only
|
||||
dependency outside the standard library. We work to keep the code lean,
|
||||
making Falcon easier to test, optimize, and deploy.
|
||||
**Light.** Only the essentials are included, with *six* and *mimeparse*
|
||||
being the only dependencies outside the standard library. We work to keep
|
||||
the code lean, making Falcon easier to test, optimize, and deploy.
|
||||
|
||||
**Flexible.** Falcon can be deployed in a variety of ways, depending on
|
||||
your needs. The framework speaks WSGI, and works great with `Python 2.6
|
||||
|
||||
@@ -18,6 +18,7 @@ limitations under the License.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import mimeparse
|
||||
import six
|
||||
|
||||
from falcon.exceptions import HTTPBadRequest
|
||||
@@ -30,6 +31,8 @@ DEFAULT_ERROR_LOG_FORMAT = (u'{0:%Y-%m-%d %H:%M:%S} [FALCON] [ERROR]'
|
||||
TRUE_STRINGS = ('true', 'True', 'yes')
|
||||
FALSE_STRINGS = ('false', 'False', 'no')
|
||||
|
||||
MEDIA_TYPES_XML = ('application/xml', 'text/xml')
|
||||
|
||||
|
||||
class InvalidHeaderValueError(HTTPBadRequest):
|
||||
def __init__(self, msg, href=None, href_text=None):
|
||||
@@ -149,19 +152,73 @@ class Request(object):
|
||||
@property
|
||||
def client_accepts_xml(self):
|
||||
"""Return True if the Accept header indicates XML support."""
|
||||
return self.client_accepts('application/xml')
|
||||
return self.client_accepts(MEDIA_TYPES_XML)
|
||||
|
||||
def client_accepts(self, media_type):
|
||||
"""Return True if the Accept header indicates a media type support."""
|
||||
def client_accepts(self, media_types):
|
||||
"""Returns the client's preferred media type.
|
||||
|
||||
accept = self._get_header_by_wsgi_name('ACCEPT')
|
||||
return ((accept is not None) and
|
||||
((media_type in accept) or ('*/*' in accept)))
|
||||
Args:
|
||||
media_types: One or more media types. May be a single string (
|
||||
of type str), or an iterable collection of strings.
|
||||
|
||||
Returns:
|
||||
True IFF the client has indicated in the Accept header that
|
||||
they accept at least one of the specified media types.
|
||||
"""
|
||||
|
||||
accept = self.accept
|
||||
|
||||
# PERF(kgriffs): Usually the following will be true, so
|
||||
# try it first.
|
||||
if isinstance(media_types, str):
|
||||
if (accept == media_types) or (accept == '*/*'):
|
||||
return accept
|
||||
|
||||
# NOTE(kgriffs): Convert to a collection to be compatible
|
||||
# with mimeparse.best_matchapplication/xhtml+xml
|
||||
media_types = (media_types,)
|
||||
|
||||
# NOTE(kgriffs): Heuristic to quickly check another common case. If
|
||||
# accept is a single type, and it is found in media_types verbatim,
|
||||
# return the media type immediately.
|
||||
elif accept in media_types:
|
||||
return accept
|
||||
|
||||
# Fall back to full-blown parsing
|
||||
preferred_type = self.client_prefers(media_types)
|
||||
return preferred_type is not None
|
||||
|
||||
def client_prefers(self, media_types):
|
||||
"""Returns the client's preferred media type given several choices.
|
||||
|
||||
Args:
|
||||
media_types: One or more media types from which to choose the
|
||||
client's preferred type. This value MUST be an iterable
|
||||
collection of strings.
|
||||
|
||||
Returns:
|
||||
The client's preferred media type, based on the Accept header,
|
||||
or None if the client does not accept any of the specified
|
||||
types.
|
||||
"""
|
||||
|
||||
try:
|
||||
# NOTE(kgriffs): best_match will return '' if no match is found
|
||||
preferred_type = mimeparse.best_match(media_types, self.accept)
|
||||
except ValueError:
|
||||
# Value for the accept header was not formatted correctly
|
||||
preferred_type = ''
|
||||
|
||||
return (preferred_type if preferred_type else None)
|
||||
|
||||
@property
|
||||
def accept(self):
|
||||
"""Value of the Accept header, or None if not found."""
|
||||
return self._get_header_by_wsgi_name('ACCEPT')
|
||||
"""Value of the Accept header, or */* if not found per RFC."""
|
||||
accept = self._get_header_by_wsgi_name('ACCEPT')
|
||||
|
||||
# NOTE(kgriffs): Per RFC, missing accept header is
|
||||
# equivalent to '*/*'
|
||||
return '*/*' if accept is None else accept
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
|
||||
@@ -91,9 +91,7 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
|
||||
'REQUEST_METHOD': method,
|
||||
'PATH_INFO': path,
|
||||
'QUERY_STRING': query_string,
|
||||
'HTTP_ACCEPT': '*/*',
|
||||
'HTTP_USER_AGENT': ('curl/7.24.0 (x86_64-apple-darwin12.0) '
|
||||
'libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5'),
|
||||
'HTTP_USER_AGENT': 'curl/7.24.0 (x86_64-apple-darwin12.0)',
|
||||
'REMOTE_PORT': '65133',
|
||||
'RAW_URI': '/',
|
||||
'REMOTE_ADDR': '127.0.0.1',
|
||||
@@ -131,12 +129,6 @@ def _add_headers_to_environ(env, headers):
|
||||
for name, value in headers.items():
|
||||
name = name.upper().replace('-', '_')
|
||||
|
||||
if value is None:
|
||||
if name == 'ACCEPT' or name == 'USER_AGENT':
|
||||
del env['HTTP_' + name]
|
||||
|
||||
continue
|
||||
|
||||
if name == 'CONTENT_TYPE':
|
||||
env[name] = value.strip()
|
||||
elif name == 'CONTENT_LENGTH':
|
||||
|
||||
@@ -175,7 +175,7 @@ class TestHTTPError(testing.TestBase):
|
||||
|
||||
def test_client_does_not_accept_anything(self):
|
||||
headers = {
|
||||
'Accept': None,
|
||||
'Accept': '45087gigo;;;;',
|
||||
'X-Error-Title': 'Storage service down',
|
||||
'X-Error-Description': ('The configured storage service is not '
|
||||
'responding to requests. Please contact '
|
||||
|
||||
@@ -87,26 +87,107 @@ class TestReqVars(testing.TestBase):
|
||||
headers = {'Accept': 'application/xml'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts('application/xml'))
|
||||
self.assertTrue(req.client_accepts(['application/xml']))
|
||||
|
||||
headers = {'Accept': '*/*'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts(['application/xml']))
|
||||
|
||||
headers = {} # NOTE(kgriffs): Equivalent to '*/*' per RFC
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts('application/xml'))
|
||||
self.assertTrue(req.client_accepts(['application/xml']))
|
||||
|
||||
headers = {'Accept': 'application/json'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertFalse(req.client_accepts('application/xml'))
|
||||
self.assertFalse(req.client_accepts(['application/xml']))
|
||||
|
||||
headers = {'Accept': 'application/xm'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertFalse(req.client_accepts('application/xml'))
|
||||
self.assertFalse(req.client_accepts(['application/xml']))
|
||||
|
||||
headers = {'Accept': 'application/*'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts(['application/json']))
|
||||
self.assertTrue(req.client_accepts(['application/xml']))
|
||||
self.assertTrue(req.client_accepts(['application/json',
|
||||
'application/xml']))
|
||||
|
||||
headers = {'Accept': 'text/*'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts('text/plain'))
|
||||
self.assertTrue(req.client_accepts('text/csv'))
|
||||
self.assertFalse(req.client_accepts('application/xhtml+xml'))
|
||||
|
||||
headers = {'Accept': 'text/*, application/xhtml+xml; q=0.0'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts('text/plain'))
|
||||
self.assertTrue(req.client_accepts('text/csv'))
|
||||
self.assertTrue(req.client_accepts('application/xhtml+xml'))
|
||||
self.assertTrue(req.client_accepts(('application/xhtml+xml',
|
||||
'text/plain',
|
||||
'text/csv')))
|
||||
|
||||
headers = {'Accept': 'text/*; q=0.1, application/xhtml+xml; q=0.5'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts('text/plain'))
|
||||
|
||||
headers = {'Accept': 'text/*, application/*'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts('text/plain'))
|
||||
self.assertTrue(req.client_accepts('application/json'))
|
||||
|
||||
headers = {'Accept': 'text/*,application/*'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts('text/plain'))
|
||||
self.assertTrue(req.client_accepts('application/json'))
|
||||
|
||||
def test_client_accepts_props(self):
|
||||
headers = {'Accept': 'application/xml'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
|
||||
self.assertTrue(req.client_accepts_xml)
|
||||
self.assertFalse(req.client_accepts_json)
|
||||
|
||||
headers = {'Accept': 'text/xml'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts_xml)
|
||||
|
||||
headers = {'Accept': 'text/*'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts_xml)
|
||||
|
||||
headers = {'Accept': 'text/xml, application/xml'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts_xml)
|
||||
|
||||
headers = {'Accept': 'application/json'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertFalse(req.client_accepts_xml)
|
||||
self.assertTrue(req.client_accepts_json)
|
||||
|
||||
headers = {'Accept': 'application/json, application/xml'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertTrue(req.client_accepts_xml)
|
||||
self.assertTrue(req.client_accepts_json)
|
||||
|
||||
def test_client_prefers(self):
|
||||
headers = {'Accept': 'application/xml'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
preferred_type = req.client_prefers(['application/xml'])
|
||||
self.assertEquals(preferred_type, 'application/xml')
|
||||
|
||||
headers = {'Accept': '*/*'}
|
||||
preferred_type = req.client_prefers(('application/xml',
|
||||
'application/json'))
|
||||
|
||||
# NOTE(kgriffs): If client doesn't care, "preferr" the first one
|
||||
self.assertEquals(preferred_type, 'application/xml')
|
||||
|
||||
headers = {'Accept': 'text/*; q=0.1, application/xhtml+xml; q=0.5'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
preferred_type = req.client_prefers(['application/xhtml+xml'])
|
||||
self.assertEquals(preferred_type, 'application/xhtml+xml')
|
||||
|
||||
def test_range(self):
|
||||
headers = {'Range': '10-'}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
@@ -124,8 +205,7 @@ class TestReqVars(testing.TestBase):
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertIs(req.range, None)
|
||||
|
||||
headers = {'Range': None}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
req = Request(testing.create_environ())
|
||||
self.assertIs(req.range, None)
|
||||
|
||||
def test_range_invalid(self):
|
||||
@@ -230,9 +310,11 @@ class TestReqVars(testing.TestBase):
|
||||
date = testing.httpnow()
|
||||
hash = 'fa0d1a60ef6616bb28038515c8ea4cb2'
|
||||
auth = 'HMAC_SHA1 c590afa9bb59191ffab30f223791e82d3fd3e3af'
|
||||
agent = 'curl/7.24.0 (x86_64-apple-darwin12.0)'
|
||||
agent = 'testing/1.0.1'
|
||||
default_agent = 'curl/7.24.0 (x86_64-apple-darwin12.0)'
|
||||
|
||||
self._test_attribute_header('Accept', 'x-falcon', 'accept')
|
||||
self._test_attribute_header('Accept', 'x-falcon', 'accept',
|
||||
default='*/*')
|
||||
|
||||
self._test_attribute_header('Authorization', auth, 'auth')
|
||||
|
||||
@@ -248,7 +330,8 @@ class TestReqVars(testing.TestBase):
|
||||
self._test_attribute_header('If-Unmodified-Since', date,
|
||||
'if_unmodified_since')
|
||||
|
||||
self._test_attribute_header('User-Agent', agent, 'user_agent')
|
||||
self._test_attribute_header('User-Agent', agent, 'user_agent',
|
||||
default=default_agent)
|
||||
|
||||
def test_method(self):
|
||||
self.assertEquals(self.req.method, 'GET')
|
||||
@@ -270,11 +353,10 @@ class TestReqVars(testing.TestBase):
|
||||
# Helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _test_attribute_header(self, name, value, attr):
|
||||
def _test_attribute_header(self, name, value, attr, default=None):
|
||||
headers = {name: value}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertEquals(getattr(req, attr), value)
|
||||
|
||||
headers = {name: None}
|
||||
req = Request(testing.create_environ(headers=headers))
|
||||
self.assertEqual(getattr(req, attr), None)
|
||||
req = Request(testing.create_environ())
|
||||
self.assertEqual(getattr(req, attr), default)
|
||||
|
||||
4
setup.py
4
setup.py
@@ -6,7 +6,9 @@ from setuptools import setup, find_packages, Extension
|
||||
VERSION = imp.load_source('version', path.join('.', 'falcon', 'version.py'))
|
||||
VERSION = VERSION.__version__
|
||||
|
||||
REQUIRES = ['six']
|
||||
# NOTE(kgriffs): python-mimeparse is newer than mimeparse, supports Py3
|
||||
# TODO(kgriffs): Fork and optimize/modernize python-mimeparse
|
||||
REQUIRES = ['six', 'python-mimeparse']
|
||||
if sys.version_info < (2, 7):
|
||||
REQUIRES.append('ordereddict')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user