Mercurial > p > roundup > code
changeset 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 | 60299cd36670 |
| children | dd709ea29899 |
| files | roundup/rest.py test/rest_common.py |
| diffstat | 2 files changed, 132 insertions(+), 5 deletions(-) [+] |
line wrap: on
line diff
--- a/roundup/rest.py Wed May 29 20:22:42 2019 -0400 +++ b/roundup/rest.py Wed May 29 22:18:46 2019 -0400 @@ -241,6 +241,12 @@ value = value.strip() if key == "q": q = float(value) + if q > 1.0: + # Not sure what to do here. Can't find spec + # about how to handle q > 1.0. Since invalid + # I choose to make it lowest in priority. + pass + q = 0.0001 else: media_params.append((key, value)) result.append((media_type, dict(media_params), q)) @@ -337,7 +343,6 @@ __accepted_content_type = { "application/json": "json", "*/*": "json", - "application/xml": "xml" } __default_accept_type = "json" @@ -361,6 +366,9 @@ self.base_path = '%srest' % (self.db.config.TRACKER_WEB) self.data_path = self.base_path + '/data' + if dicttoxml: # add xml if supported + self.__accepted_content_type["application/xml"] = "xml" + def props_from_args(self, cl, args, itemid=None, skip_protected=True): """Construct a list of properties from the given arguments, and return them after validation. @@ -1729,9 +1737,18 @@ method.upper(), uri) # parse Accept header and get the content type + # Acceptable types ordered with preferred one first + # in list. accept_header = parse_accept_header(headers.get('Accept')) - accept_type = self.__default_accept_type + if not accept_header: + accept_type = self.__default_accept_type + else: + accept_type = None for part in accept_header: + if accept_type: + # we accepted the best match, stop searching for + # lower quality matches. + break if part[0] in self.__accepted_content_type: accept_type = self.__accepted_content_type[part[0]] # Version order: @@ -1762,7 +1779,7 @@ # header (Accept: application/json, application/xml) # default (application/json) ext_type = os.path.splitext(urlparse(uri).path)[1][1:] - data_type = ext_type or accept_type + data_type = ext_type or accept_type or "invalid" if ( ext_type ): # strip extension so uri make sense @@ -1893,7 +1910,9 @@ # error out before doing any work if we can't # display acceptable output. self.client.response_code = 406 - output = "Content type is not accepted by client" + output = ( "Requested content type is not available.\n" + "Acceptable types: %s"%( + ", ".join(self.__accepted_content_type.keys()))) # Make output json end in a newline to # separate from following text in logs etc..
--- a/test/rest_common.py Wed May 29 20:22:42 2019 -0400 +++ b/test/rest_common.py Wed May 29 22:18:46 2019 -0400 @@ -1368,6 +1368,114 @@ del(self.headers) + def testAcceptHeaderParsing(self): + # TEST #1 + # json highest priority + self.server.client.request.headers.get=self.get_header + headers={"accept": "application/json; version=1," + "application/xml; q=0.5; version=2," + "text/plain; q=0.75; version=2" + } + self.headers=headers + results = self.server.dispatch('GET', + "/rest/data/status/1", + self.empty_form) + print(results) + self.assertEqual(self.server.client.response_code, 200) + self.assertEqual(self.server.client.additional_headers['Content-Type'], + "application/json") + + # TEST #2 + # text highest priority + headers={"accept": "application/json; q=0.5; version=1," + "application/xml; q=0.25; version=2," + "text/plain; q=1.0; version=3" + } + self.headers=headers + results = self.server.dispatch('GET', + "/rest/data/status/1", + self.empty_form) + print(results) + self.assertEqual(self.server.client.response_code, 200) + self.assertEqual(self.server.client.additional_headers['Content-Type'], + "application/json") + + # TEST #3 + # no acceptable type + headers={"accept": "text/plain; q=1.0; version=2" + } + self.headers=headers + results = self.server.dispatch('GET', + "/rest/data/status/1", + self.empty_form) + print(results) + self.assertEqual(self.server.client.response_code, 406) + self.assertEqual(self.server.client.additional_headers['Content-Type'], + "application/json") + + # TEST #4 + # no accept header, should use default json + headers={} + self.headers=headers + results = self.server.dispatch('GET', + "/rest/data/status/1", + self.empty_form) + print(results) + self.assertEqual(self.server.client.response_code, 200) + self.assertEqual(self.server.client.additional_headers['Content-Type'], + "application/json") + + # TEST #5 + # wildcard accept header, should use default json + headers={ "accept": "*/*"} + self.headers=headers + results = self.server.dispatch('GET', + "/rest/data/status/1", + self.empty_form) + print(results) + self.assertEqual(self.server.client.response_code, 200) + self.assertEqual(self.server.client.additional_headers['Content-Type'], + "application/json") + + # TEST #6 + # invalid q factor if not ignored/demoted + # application/json is selected with invalid version + # and errors. + # this ends up choosing */* which triggers json. + self.server.client.request.headers.get=self.get_header + headers={"accept": "application/json; q=1.5; version=99," + "*/*; q=0.9; version=1," + "text/plain; q=3.75; version=2" + } + self.headers=headers + results = self.server.dispatch('GET', + "/rest/data/status/1", + self.empty_form) + print(results) + self.assertEqual(self.server.client.response_code, 200) + self.assertEqual(self.server.client.additional_headers['Content-Type'], + "application/json") + + + ''' + # only works if dicttoxml.py is installed. + # not installed for testing + # TEST #7 + # xml wins + headers={"accept": "application/json; q=0.5; version=2," + "application/xml; q=0.75; version=1," + "text/plain; q=1.0; version=2" + } + self.headers=headers + results = self.server.dispatch('GET', + "/rest/data/status/1", + self.empty_form) + print(results) + self.assertEqual(self.server.client.response_code, 200) + self.assertEqual(self.server.client.additional_headers['Content-Type'], + "application/xml") + ''' + def testMethodOverride(self): # TEST #1 # Use GET, PUT, PATCH to tunnel DELETE expect error @@ -1408,7 +1516,7 @@ etag = calculate_etag(self.db.status.getnode("1"), self.db.config['WEB_SECRET_KEY']) etagb = etag.strip ('"') - headers={"accept": "application/json; q=0.75, application/xml; q=1", + headers={"accept": "application/json; q=1.0, application/xml; q=0.75", "if-match": '"%s"'%etagb, "content-length": 0, "x-http-method-override": "DElETE"
