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

View File

@ -586,38 +586,49 @@ class Accept(object):
:param headerval: value of the header as a str :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): def __init__(self, headerval):
self.headerval = headerval self.headerval = headerval
def _get_types(self): def _get_types(self):
headerval = self.headerval or '*/*'
level = 1
types = [] types = []
for typ in headerval.split(','): if not self.headerval:
quality = 1.0 return []
if '; q=' in typ: for typ in self.headerval.split(','):
typ, quality = typ.split('; q=') type_parms = self.acc_pattern.findall(typ)
elif ';q=' in typ: if not type_parms:
typ, quality = typ.split(';q=') raise ValueError('Invalid accept header')
quality = float(quality) typ, subtype, parms, quality = type_parms[0]
if typ.startswith('*/'): quality = float(quality or '1.0')
quality -= 0.01 pattern = '^' + \
elif typ.endswith('/*'): (self.token if typ == '*' else re.escape(typ)) + '/' + \
quality -= 0.01 (self.token if subtype == '*' else re.escape(subtype)) + '$'
elif '*' in typ: types.append((pattern, quality, '*' not in (typ, subtype)))
raise AssertionError('bad accept header') # sort candidates by quality, then whether or not there were globs
pattern = '[a-zA-Z0-9-]+'.join([re.escape(x) for x in types.sort(reverse=True, key=lambda t: (t[1], t[2]))
typ.strip().split('*')]) return [t[0] for t in types]
types.append((quality, re.compile(pattern), typ))
types.sort(reverse=True, key=lambda t: t[0])
return types
def best_match(self, options, default_match='text/plain'): def best_match(self, options):
for quality, pattern, typ in self._get_types(): """
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: for option in options:
if pattern.match(option): if re.match(pattern, option):
return option return option
return default_match return None
def __repr__(self): def __repr__(self):
return self.headerval return self.headerval
@ -1011,6 +1022,7 @@ HTTPUnauthorized = status_map[401]
HTTPForbidden = status_map[403] HTTPForbidden = status_map[403]
HTTPMethodNotAllowed = status_map[405] HTTPMethodNotAllowed = status_map[405]
HTTPNotFound = status_map[404] HTTPNotFound = status_map[404]
HTTPNotAcceptable = status_map[406]
HTTPRequestTimeout = status_map[408] HTTPRequestTimeout = status_map[408]
HTTPConflict = status_map[409] HTTPConflict = status_map[409]
HTTPLengthRequired = status_map[411] 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, \ from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPConflict, \
HTTPCreated, HTTPInternalServerError, HTTPNoContent, HTTPNotFound, \ HTTPCreated, HTTPInternalServerError, HTTPNoContent, HTTPNotFound, \
HTTPPreconditionFailed, HTTPMethodNotAllowed, Request, Response, \ HTTPPreconditionFailed, HTTPMethodNotAllowed, Request, Response, \
HTTPInsufficientStorage HTTPInsufficientStorage, HTTPNotAcceptable
DATADIR = 'containers' DATADIR = 'containers'
@ -280,14 +280,10 @@ class ContainerController(object):
if get_param(req, 'format'): if get_param(req, 'format'):
req.accept = FORMAT2CONTENT_TYPE.get( req.accept = FORMAT2CONTENT_TYPE.get(
get_param(req, 'format').lower(), FORMAT2CONTENT_TYPE['plain']) get_param(req, 'format').lower(), FORMAT2CONTENT_TYPE['plain'])
try: headers['Content-Type'] = req.accept.best_match(
headers['Content-Type'] = req.accept.best_match( ['text/plain', 'application/json', 'application/xml', 'text/xml'])
['text/plain', 'application/json', 'application/xml', if not headers['Content-Type']:
'text/xml'], return HTTPNotAcceptable(request=req)
default_match='text/plain')
except AssertionError, err:
return HTTPBadRequest(body='bad accept header: %s' % req.accept,
content_type='text/plain', request=req)
return HTTPNoContent(request=req, headers=headers, charset='utf-8') return HTTPNoContent(request=req, headers=headers, charset='utf-8')
@public @public
@ -344,14 +340,10 @@ class ContainerController(object):
if query_format: if query_format:
req.accept = FORMAT2CONTENT_TYPE.get(query_format.lower(), req.accept = FORMAT2CONTENT_TYPE.get(query_format.lower(),
FORMAT2CONTENT_TYPE['plain']) FORMAT2CONTENT_TYPE['plain'])
try: out_content_type = req.accept.best_match(
out_content_type = req.accept.best_match( ['text/plain', 'application/json', 'application/xml', 'text/xml'])
['text/plain', 'application/json', 'application/xml', if not out_content_type:
'text/xml'], return HTTPNotAcceptable(request=req)
default_match='text/plain')
except AssertionError, err:
return HTTPBadRequest(body='bad accept header: %s' % req.accept,
content_type='text/plain', request=req)
container_list = broker.list_objects_iter(limit, marker, end_marker, container_list = broker.list_objects_iter(limit, marker, end_marker,
prefix, delimiter, path) prefix, delimiter, path)
if out_content_type == 'application/json': 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 = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'GET'})
req.accept = 'application/xml*' req.accept = 'application/xml*'
resp = self.controller.GET(req) resp = self.controller.GET(req)
self.assertEquals(resp.status_int, 400) self.assertEquals(resp.status_int, 406)
self.assertEquals(resp.body, 'bad accept header: application/xml*')
def test_GET_prefix_delimeter_plain(self): def test_GET_prefix_delimeter_plain(self):
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT', req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT',

View File

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

View File

@ -797,7 +797,7 @@ class TestContainerController(unittest.TestCase):
resp = self.controller.GET(req) resp = self.controller.GET(req)
result = [x['content_type'] for x in simplejson.loads(resp.body)] result = [x['content_type'] for x in simplejson.loads(resp.body)]
self.assertEquals(result, [u'\u2603', 'text/plain; "utf-8"']) self.assertEquals(result, [u'\u2603', 'text/plain; "utf-8"'])
def test_GET_accept_not_valid(self): def test_GET_accept_not_valid(self):
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT', req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT',
'HTTP_X_TIMESTAMP': '0'}) 'HTTP_X_TIMESTAMP': '0'})
@ -812,9 +812,8 @@ class TestContainerController(unittest.TestCase):
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'GET'}) req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'GET'})
req.accept = 'application/xml*' req.accept = 'application/xml*'
resp = self.controller.GET(req) resp = self.controller.GET(req)
self.assertEquals(resp.status_int, 400) self.assertEquals(resp.status_int, 406)
self.assertEquals(resp.body, 'bad accept header: application/xml*')
def test_GET_limit(self): def test_GET_limit(self):
# make a container # make a container
req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT', req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'PUT',