Skip to content

Commit 3586518

Browse files
committed
Accept header parsing fixes. Now return first acceptable match rather
than last. If not acceptable match in accept, 406 error returns list of acceptable types as text string. application/xml is listed in acceptable types only if dicttoxml is installed. Handle q > 1.0 by demoting q factor to 0.0001 making it unusable. Test cases for all this code. XML is commented out as we don't install dicttoxml.py.
1 parent 03d1e64 commit 3586518

File tree

2 files changed

+132
-5
lines changed

2 files changed

+132
-5
lines changed

roundup/rest.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ def parse_accept_header(accept):
241241
value = value.strip()
242242
if key == "q":
243243
q = float(value)
244+
if q > 1.0:
245+
# Not sure what to do here. Can't find spec
246+
# about how to handle q > 1.0. Since invalid
247+
# I choose to make it lowest in priority.
248+
pass
249+
q = 0.0001
244250
else:
245251
media_params.append((key, value))
246252
result.append((media_type, dict(media_params), q))
@@ -337,7 +343,6 @@ class RestfulInstance(object):
337343
__accepted_content_type = {
338344
"application/json": "json",
339345
"*/*": "json",
340-
"application/xml": "xml"
341346
}
342347
__default_accept_type = "json"
343348

@@ -361,6 +366,9 @@ def __init__(self, client, db):
361366
self.base_path = '%srest' % (self.db.config.TRACKER_WEB)
362367
self.data_path = self.base_path + '/data'
363368

369+
if dicttoxml: # add xml if supported
370+
self.__accepted_content_type["application/xml"] = "xml"
371+
364372
def props_from_args(self, cl, args, itemid=None, skip_protected=True):
365373
"""Construct a list of properties from the given arguments,
366374
and return them after validation.
@@ -1729,9 +1737,18 @@ def dispatch(self, method, uri, input):
17291737
method.upper(), uri)
17301738

17311739
# parse Accept header and get the content type
1740+
# Acceptable types ordered with preferred one first
1741+
# in list.
17321742
accept_header = parse_accept_header(headers.get('Accept'))
1733-
accept_type = self.__default_accept_type
1743+
if not accept_header:
1744+
accept_type = self.__default_accept_type
1745+
else:
1746+
accept_type = None
17341747
for part in accept_header:
1748+
if accept_type:
1749+
# we accepted the best match, stop searching for
1750+
# lower quality matches.
1751+
break
17351752
if part[0] in self.__accepted_content_type:
17361753
accept_type = self.__accepted_content_type[part[0]]
17371754
# Version order:
@@ -1762,7 +1779,7 @@ def dispatch(self, method, uri, input):
17621779
# header (Accept: application/json, application/xml)
17631780
# default (application/json)
17641781
ext_type = os.path.splitext(urlparse(uri).path)[1][1:]
1765-
data_type = ext_type or accept_type
1782+
data_type = ext_type or accept_type or "invalid"
17661783

17671784
if ( ext_type ):
17681785
# strip extension so uri make sense
@@ -1893,7 +1910,9 @@ def dispatch(self, method, uri, input):
18931910
# error out before doing any work if we can't
18941911
# display acceptable output.
18951912
self.client.response_code = 406
1896-
output = "Content type is not accepted by client"
1913+
output = ( "Requested content type is not available.\n"
1914+
"Acceptable types: %s"%(
1915+
", ".join(self.__accepted_content_type.keys())))
18971916

18981917
# Make output json end in a newline to
18991918
# separate from following text in logs etc..

test/rest_common.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,114 @@ def testDispatch(self):
13681368

13691369
del(self.headers)
13701370

1371+
def testAcceptHeaderParsing(self):
1372+
# TEST #1
1373+
# json highest priority
1374+
self.server.client.request.headers.get=self.get_header
1375+
headers={"accept": "application/json; version=1,"
1376+
"application/xml; q=0.5; version=2,"
1377+
"text/plain; q=0.75; version=2"
1378+
}
1379+
self.headers=headers
1380+
results = self.server.dispatch('GET',
1381+
"/rest/data/status/1",
1382+
self.empty_form)
1383+
print(results)
1384+
self.assertEqual(self.server.client.response_code, 200)
1385+
self.assertEqual(self.server.client.additional_headers['Content-Type'],
1386+
"application/json")
1387+
1388+
# TEST #2
1389+
# text highest priority
1390+
headers={"accept": "application/json; q=0.5; version=1,"
1391+
"application/xml; q=0.25; version=2,"
1392+
"text/plain; q=1.0; version=3"
1393+
}
1394+
self.headers=headers
1395+
results = self.server.dispatch('GET',
1396+
"/rest/data/status/1",
1397+
self.empty_form)
1398+
print(results)
1399+
self.assertEqual(self.server.client.response_code, 200)
1400+
self.assertEqual(self.server.client.additional_headers['Content-Type'],
1401+
"application/json")
1402+
1403+
# TEST #3
1404+
# no acceptable type
1405+
headers={"accept": "text/plain; q=1.0; version=2"
1406+
}
1407+
self.headers=headers
1408+
results = self.server.dispatch('GET',
1409+
"/rest/data/status/1",
1410+
self.empty_form)
1411+
print(results)
1412+
self.assertEqual(self.server.client.response_code, 406)
1413+
self.assertEqual(self.server.client.additional_headers['Content-Type'],
1414+
"application/json")
1415+
1416+
# TEST #4
1417+
# no accept header, should use default json
1418+
headers={}
1419+
self.headers=headers
1420+
results = self.server.dispatch('GET',
1421+
"/rest/data/status/1",
1422+
self.empty_form)
1423+
print(results)
1424+
self.assertEqual(self.server.client.response_code, 200)
1425+
self.assertEqual(self.server.client.additional_headers['Content-Type'],
1426+
"application/json")
1427+
1428+
# TEST #5
1429+
# wildcard accept header, should use default json
1430+
headers={ "accept": "*/*"}
1431+
self.headers=headers
1432+
results = self.server.dispatch('GET',
1433+
"/rest/data/status/1",
1434+
self.empty_form)
1435+
print(results)
1436+
self.assertEqual(self.server.client.response_code, 200)
1437+
self.assertEqual(self.server.client.additional_headers['Content-Type'],
1438+
"application/json")
1439+
1440+
# TEST #6
1441+
# invalid q factor if not ignored/demoted
1442+
# application/json is selected with invalid version
1443+
# and errors.
1444+
# this ends up choosing */* which triggers json.
1445+
self.server.client.request.headers.get=self.get_header
1446+
headers={"accept": "application/json; q=1.5; version=99,"
1447+
"*/*; q=0.9; version=1,"
1448+
"text/plain; q=3.75; version=2"
1449+
}
1450+
self.headers=headers
1451+
results = self.server.dispatch('GET',
1452+
"/rest/data/status/1",
1453+
self.empty_form)
1454+
print(results)
1455+
self.assertEqual(self.server.client.response_code, 200)
1456+
self.assertEqual(self.server.client.additional_headers['Content-Type'],
1457+
"application/json")
1458+
1459+
1460+
'''
1461+
# only works if dicttoxml.py is installed.
1462+
# not installed for testing
1463+
# TEST #7
1464+
# xml wins
1465+
headers={"accept": "application/json; q=0.5; version=2,"
1466+
"application/xml; q=0.75; version=1,"
1467+
"text/plain; q=1.0; version=2"
1468+
}
1469+
self.headers=headers
1470+
results = self.server.dispatch('GET',
1471+
"/rest/data/status/1",
1472+
self.empty_form)
1473+
print(results)
1474+
self.assertEqual(self.server.client.response_code, 200)
1475+
self.assertEqual(self.server.client.additional_headers['Content-Type'],
1476+
"application/xml")
1477+
'''
1478+
13711479
def testMethodOverride(self):
13721480
# TEST #1
13731481
# Use GET, PUT, PATCH to tunnel DELETE expect error
@@ -1408,7 +1516,7 @@ def testMethodOverride(self):
14081516
etag = calculate_etag(self.db.status.getnode("1"),
14091517
self.db.config['WEB_SECRET_KEY'])
14101518
etagb = etag.strip ('"')
1411-
headers={"accept": "application/json; q=0.75, application/xml; q=1",
1519+
headers={"accept": "application/json; q=1.0, application/xml; q=0.75",
14121520
"if-match": '"%s"'%etagb,
14131521
"content-length": 0,
14141522
"x-http-method-override": "DElETE"

0 commit comments

Comments
 (0)