Merge "406 if we can't satisfy Accept"

This commit is contained in:
Jenkins 2012-12-03 23:20:10 +00:00 committed by Gerrit Code Review
commit 8a6922b73e
6 changed files with 73 additions and 70 deletions

View File

@ -35,7 +35,7 @@ from swift.common.swob import HTTPAccepted, HTTPBadRequest, \
HTTPCreated, HTTPForbidden, HTTPInternalServerError, \
HTTPMethodNotAllowed, HTTPNoContent, HTTPNotFound, \
HTTPPreconditionFailed, HTTPConflict, Request, Response, \
HTTPInsufficientStorage
HTTPInsufficientStorage, HTTPNotAcceptable
DATADIR = 'accounts'
@ -182,14 +182,10 @@ class AccountController(object):
if get_param(req, 'format'):
req.accept = FORMAT2CONTENT_TYPE.get(
get_param(req, 'format').lower(), FORMAT2CONTENT_TYPE['plain'])
try:
headers['Content-Type'] = req.accept.best_match(
['text/plain', 'application/json', 'application/xml',
'text/xml'],
default_match='text/plain')
except AssertionError, err:
return HTTPBadRequest(body='bad accept header: %s' % req.accept,
content_type='text/plain', request=req)
headers['Content-Type'] = req.accept.best_match(
['text/plain', 'application/json', 'application/xml', 'text/xml'])
if not headers['Content-Type']:
return HTTPNotAcceptable(request=req)
return HTTPNoContent(request=req, headers=headers, charset='utf-8')
@public
@ -242,14 +238,10 @@ class AccountController(object):
if query_format:
req.accept = FORMAT2CONTENT_TYPE.get(query_format.lower(),
FORMAT2CONTENT_TYPE['plain'])
try:
out_content_type = req.accept.best_match(
['text/plain', 'application/json', 'application/xml',
'text/xml'],
default_match='text/plain')
except AssertionError, err:
return HTTPBadRequest(body='bad accept header: %s' % req.accept,
content_type='text/plain', request=req)
out_content_type = req.accept.best_match(
['text/plain', 'application/json', 'application/xml', 'text/xml'])
if not out_content_type:
return HTTPNotAcceptable(request=req)
account_list = broker.list_containers_iter(limit, marker, end_marker,
prefix, delimiter)
if out_content_type == 'application/json':

View File

@ -586,38 +586,49 @@ class Accept(object):
:param headerval: value of the header as a str
"""
token = r'[^()<>@,;:\"/\[\]?={}\x00-\x20\x7f]+' # RFC 2616 2.2
acc_pattern = re.compile(r'^\s*(' + token + r')/(' + token +
r')(;\s*q=([\d.]+))?\s*$')
def __init__(self, headerval):
self.headerval = headerval
def _get_types(self):
headerval = self.headerval or '*/*'
level = 1
types = []
for typ in headerval.split(','):
quality = 1.0
if '; q=' in typ:
typ, quality = typ.split('; q=')
elif ';q=' in typ:
typ, quality = typ.split(';q=')
quality = float(quality)
if typ.startswith('*/'):
quality -= 0.01
elif typ.endswith('/*'):
quality -= 0.01
elif '*' in typ:
raise AssertionError('bad accept header')
pattern = '[a-zA-Z0-9-]+'.join([re.escape(x) for x in
typ.strip().split('*')])
types.append((quality, re.compile(pattern), typ))
types.sort(reverse=True, key=lambda t: t[0])
return types
if not self.headerval:
return []
for typ in self.headerval.split(','):
type_parms = self.acc_pattern.findall(typ)
if not type_parms:
raise ValueError('Invalid accept header')
typ, subtype, parms, quality = type_parms[0]
quality = float(quality or '1.0')
pattern = '^' + \
(self.token if typ == '*' else re.escape(typ)) + '/' + \
(self.token if subtype == '*' else re.escape(subtype)) + '$'
types.append((pattern, quality, '*' not in (typ, subtype)))
# sort candidates by quality, then whether or not there were globs
types.sort(reverse=True, key=lambda t: (t[1], t[2]))
return [t[0] for t in types]
def best_match(self, options, default_match='text/plain'):
for quality, pattern, typ in self._get_types():
def best_match(self, options):
"""
Returns the item from "options" that best matches the accept header.
Returns None if no available options are acceptable to the client.
:param options: a list of content-types the server can respond with
"""
try:
types = self._get_types()
except ValueError:
return None
if not types and options:
return options[0]
for pattern in types:
for option in options:
if pattern.match(option):
if re.match(pattern, option):
return option
return default_match
return None
def __repr__(self):
return self.headerval
@ -1011,6 +1022,7 @@ HTTPUnauthorized = status_map[401]
HTTPForbidden = status_map[403]
HTTPMethodNotAllowed = status_map[405]
HTTPNotFound = status_map[404]
HTTPNotAcceptable = status_map[406]
HTTPRequestTimeout = status_map[408]
HTTPConflict = status_map[409]
HTTPLengthRequired = status_map[411]

View File

@ -38,7 +38,7 @@ from swift.common.http import HTTP_NOT_FOUND, is_success
from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPConflict, \
HTTPCreated, HTTPInternalServerError, HTTPNoContent, HTTPNotFound, \
HTTPPreconditionFailed, HTTPMethodNotAllowed, Request, Response, \
HTTPInsufficientStorage
HTTPInsufficientStorage, HTTPNotAcceptable
DATADIR = 'containers'
@ -280,14 +280,10 @@ class ContainerController(object):
if get_param(req, 'format'):
req.accept = FORMAT2CONTENT_TYPE.get(
get_param(req, 'format').lower(), FORMAT2CONTENT_TYPE['plain'])
try:
headers['Content-Type'] = req.accept.best_match(
['text/plain', 'application/json', 'application/xml',
'text/xml'],
default_match='text/plain')
except AssertionError, err:
return HTTPBadRequest(body='bad accept header: %s' % req.accept,
content_type='text/plain', request=req)
headers['Content-Type'] = req.accept.best_match(
['text/plain', 'application/json', 'application/xml', 'text/xml'])
if not headers['Content-Type']:
return HTTPNotAcceptable(request=req)
return HTTPNoContent(request=req, headers=headers, charset='utf-8')
@public
@ -344,14 +340,10 @@ class ContainerController(object):
if query_format:
req.accept = FORMAT2CONTENT_TYPE.get(query_format.lower(),
FORMAT2CONTENT_TYPE['plain'])
try:
out_content_type = req.accept.best_match(
['text/plain', 'application/json', 'application/xml',
'text/xml'],
default_match='text/plain')
except AssertionError, err:
return HTTPBadRequest(body='bad accept header: %s' % req.accept,
content_type='text/plain', request=req)
out_content_type = req.accept.best_match(
['text/plain', 'application/json', 'application/xml', 'text/xml'])
if not out_content_type:
return HTTPNotAcceptable(request=req)
container_list = broker.list_objects_iter(limit, marker, end_marker,
prefix, delimiter, path)
if out_content_type == 'application/json':

View File

@ -771,8 +771,7 @@ class TestAccountController(unittest.TestCase):
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'GET'})
req.accept = 'application/xml*'
resp = self.controller.GET(req)
self.assertEquals(resp.status_int, 400)
self.assertEquals(resp.body, 'bad accept header: application/xml*')
self.assertEquals(resp.status_int, 406)
def test_GET_prefix_delimeter_plain(self):
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT',

View File

@ -232,11 +232,12 @@ class TestMatch(unittest.TestCase):
class TestAccept(unittest.TestCase):
def test_accept_json(self):
for accept in ('application/json', 'application/json;q=1.0,*/*;q=0.9',
'*/*;q=0.9,application/json;q=1.0', 'application/*'):
'*/*;q=0.9,application/json;q=1.0', 'application/*',
'text/*,application/json', 'application/*,text/*',
'application/json,text/xml'):
acc = swift.common.swob.Accept(accept)
match = acc.best_match(['text/plain', 'application/json',
'application/xml', 'text/xml'],
default_match='text/plain')
'application/xml', 'text/xml'])
self.assertEquals(match, 'application/json')
def test_accept_plain(self):
@ -245,8 +246,7 @@ class TestAccept(unittest.TestCase):
'text/plain,application/xml'):
acc = swift.common.swob.Accept(accept)
match = acc.best_match(['text/plain', 'application/json',
'application/xml', 'text/xml'],
default_match='text/plain')
'application/xml', 'text/xml'])
self.assertEquals(match, 'text/plain')
def test_accept_xml(self):
@ -254,9 +254,18 @@ class TestAccept(unittest.TestCase):
'*/*;q=0.9,application/xml;q=1.0'):
acc = swift.common.swob.Accept(accept)
match = acc.best_match(['text/plain', 'application/xml',
'text/xml'], default_match='text/plain')
'text/xml'])
self.assertEquals(match, 'application/xml')
def test_accept_invalid(self):
for accept in ('*', 'text/plain,,', 'some stuff',
'application/xml;q=1.0;q=1.1', 'text/plain,*',
'text /plain', 'text\x7f/plain'):
acc = swift.common.swob.Accept(accept)
match = acc.best_match(['text/plain', 'application/xml',
'text/xml'])
self.assertEquals(match, None)
class TestRequest(unittest.TestCase):
def test_blank(self):

View File

@ -812,8 +812,7 @@ class TestContainerController(unittest.TestCase):
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'GET'})
req.accept = 'application/xml*'
resp = self.controller.GET(req)
self.assertEquals(resp.status_int, 400)
self.assertEquals(resp.body, 'bad accept header: application/xml*')
self.assertEquals(resp.status_int, 406)
def test_GET_limit(self):
# make a container