comparison roundup/rest.py @ 5744:d4de45cde106

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.
author John Rouillard <rouilj@ieee.org>
date Wed, 29 May 2019 22:18:46 -0400
parents abbea26a11df
children dd709ea29899
comparison
equal deleted inserted replaced
5743:60299cd36670 5744:d4de45cde106
239 (key, value) = part.lstrip().split("=", 1) 239 (key, value) = part.lstrip().split("=", 1)
240 key = key.strip() 240 key = key.strip()
241 value = value.strip() 241 value = value.strip()
242 if key == "q": 242 if key == "q":
243 q = float(value) 243 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
244 else: 250 else:
245 media_params.append((key, value)) 251 media_params.append((key, value))
246 result.append((media_type, dict(media_params), q)) 252 result.append((media_type, dict(media_params), q))
247 result.sort(key=lambda x: x[2], reverse=True) 253 result.sort(key=lambda x: x[2], reverse=True)
248 return result 254 return result
335 341
336 __default_patch_op = "replace" # default operator for PATCH method 342 __default_patch_op = "replace" # default operator for PATCH method
337 __accepted_content_type = { 343 __accepted_content_type = {
338 "application/json": "json", 344 "application/json": "json",
339 "*/*": "json", 345 "*/*": "json",
340 "application/xml": "xml"
341 } 346 }
342 __default_accept_type = "json" 347 __default_accept_type = "json"
343 348
344 __default_api_version = 1 349 __default_api_version = 1
345 __supported_api_versions = [ 1 ] 350 __supported_api_versions = [ 1 ]
358 self.actions = dict (retire = actions.Retire, restore = actions.Restore) 363 self.actions = dict (retire = actions.Retire, restore = actions.Restore)
359 364
360 # note TRACKER_WEB ends in a / 365 # note TRACKER_WEB ends in a /
361 self.base_path = '%srest' % (self.db.config.TRACKER_WEB) 366 self.base_path = '%srest' % (self.db.config.TRACKER_WEB)
362 self.data_path = self.base_path + '/data' 367 self.data_path = self.base_path + '/data'
368
369 if dicttoxml: # add xml if supported
370 self.__accepted_content_type["application/xml"] = "xml"
363 371
364 def props_from_args(self, cl, args, itemid=None, skip_protected=True): 372 def props_from_args(self, cl, args, itemid=None, skip_protected=True):
365 """Construct a list of properties from the given arguments, 373 """Construct a list of properties from the given arguments,
366 and return them after validation. 374 and return them after validation.
367 375
1727 logger.info( 1735 logger.info(
1728 'Ignoring X-HTTP-Method-Override using %s request on %s', 1736 'Ignoring X-HTTP-Method-Override using %s request on %s',
1729 method.upper(), uri) 1737 method.upper(), uri)
1730 1738
1731 # parse Accept header and get the content type 1739 # parse Accept header and get the content type
1740 # Acceptable types ordered with preferred one first
1741 # in list.
1732 accept_header = parse_accept_header(headers.get('Accept')) 1742 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
1734 for part in accept_header: 1747 for part in accept_header:
1748 if accept_type:
1749 # we accepted the best match, stop searching for
1750 # lower quality matches.
1751 break
1735 if part[0] in self.__accepted_content_type: 1752 if part[0] in self.__accepted_content_type:
1736 accept_type = self.__accepted_content_type[part[0]] 1753 accept_type = self.__accepted_content_type[part[0]]
1737 # Version order: 1754 # Version order:
1738 # 1) accept header version=X specifier 1755 # 1) accept header version=X specifier
1739 # application/vnd.x.y; version=1 1756 # application/vnd.x.y; version=1
1760 # get the request format for response 1777 # get the request format for response
1761 # priority : extension from uri (/rest/data/issue.json), 1778 # priority : extension from uri (/rest/data/issue.json),
1762 # header (Accept: application/json, application/xml) 1779 # header (Accept: application/json, application/xml)
1763 # default (application/json) 1780 # default (application/json)
1764 ext_type = os.path.splitext(urlparse(uri).path)[1][1:] 1781 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"
1766 1783
1767 if ( ext_type ): 1784 if ( ext_type ):
1768 # strip extension so uri make sense 1785 # strip extension so uri make sense
1769 # .../issue.json -> .../issue 1786 # .../issue.json -> .../issue
1770 uri = uri[:-( len(ext_type) + 1 )] 1787 uri = uri[:-( len(ext_type) + 1 )]
1891 else: 1908 else:
1892 # FIXME?? consider moving this earlier. We should 1909 # FIXME?? consider moving this earlier. We should
1893 # error out before doing any work if we can't 1910 # error out before doing any work if we can't
1894 # display acceptable output. 1911 # display acceptable output.
1895 self.client.response_code = 406 1912 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())))
1897 1916
1898 # Make output json end in a newline to 1917 # Make output json end in a newline to
1899 # separate from following text in logs etc.. 1918 # separate from following text in logs etc..
1900 return bs2b(output + "\n") 1919 return bs2b(output + "\n")
1901 1920

Roundup Issue Tracker: http://roundup-tracker.org/