@@ -268,6 +268,8 @@ def parse_accept_header(accept):
268268
269269 Default `q` for values that are not specified is 1.0
270270
271+ If q value > 1.0, it is parsed as a very small value.
272+
271273 # Based on https://gist.github.com/samuraisam/2714195
272274 # Also, based on a snipped found in this project:
273275 # https://github.com/martinblech/mimerender
@@ -2206,6 +2208,136 @@ def handle_apiRateLimitExceeded(self, apiRateLimit):
22062208 # human may read it
22072209 ), None )
22082210
2211+ def determine_output_format (self , uri ):
2212+ """Returns tuple of:
2213+
2214+ (format for returned output,
2215+ possibly modified uri,
2216+ output error object (if error else None))
2217+
2218+ Verify that client is requesting an output we can produce
2219+ with a version we support.
2220+
2221+ Only application/json and application/xml (optional)
2222+ are currently supported. .vcard might be useful
2223+ in the future for user objects.
2224+
2225+ Find format for returned output:
2226+
2227+ 1) Get format from url extension (.json, .xml) and return
2228+ if invalid return (None, uri, 406 error)
2229+ if not found continue
2230+ 2) Parse Accept header obeying q values
2231+ if header unparsible return 400 error object.
2232+ 3) if empty or missing Accept header
2233+ return self.__default_accept_type
2234+ 4) match and return best Accept header/version
2235+ if version error found in matching type return 406 error
2236+ 5) if no requested format is supported return 406
2237+ error
2238+
2239+ """
2240+ # get the request format for response
2241+ # priority : extension from uri (/rest/data/issue.json)
2242+ # only json or xml valid at this time.
2243+ # header (Accept: application/json, application/xml)
2244+ # default (application/json)
2245+ ext_type = os .path .splitext (urlparse (uri ).path )[1 ][1 :]
2246+
2247+ # Check to see if the extension matches a value in
2248+ # self.__accepted_content_type. In the future other output
2249+ # such as .vcard for downloading user info can be
2250+ # supported. This method also allow detection of mistyped
2251+ # types like jon for json. Limit extension length to less than
2252+ # 10 characters to allow passing JWT via URL path. Could be
2253+ # useful for magic link login method or account recovery workflow,
2254+ # using a JWT with a short expiration time and limited rights
2255+ # (e.g. only password change permission))
2256+ if ext_type and (len (ext_type ) < 10 ):
2257+ if ext_type not in list (self .__accepted_content_type .values ()):
2258+ self .client .response_code = 406
2259+ return (None , uri ,
2260+ self .error_obj (
2261+ 406 ,
2262+ _ ("Content type '%s' requested in URL is "
2263+ "not available.\n Acceptable types: %s\n " ) %
2264+ (ext_type ,
2265+ ", " .join (sorted (
2266+ set (self .__accepted_content_type .values ()))))))
2267+
2268+ # strip extension so uri makes sense.
2269+ # E.G. .../issue.json -> .../issue
2270+ uri = uri [:- (len (ext_type ) + 1 )]
2271+ return (ext_type , uri , None )
2272+
2273+ # parse Accept header and get the content type
2274+ # Acceptable types ordered with preferred one (by q value)
2275+ # first in list.
2276+ try :
2277+ accept_header = parse_accept_header (
2278+ self .client .request .headers .get ('Accept' )
2279+ )
2280+ except UsageError as e :
2281+ self .client .response_code = 406
2282+ return (None , uri , self .error_obj (
2283+ 400 , _ ("Unable to parse Accept Header. %(error)s. "
2284+ "Acceptable types: %(acceptable_types)s" ) % {
2285+ 'error' : e .args [0 ],
2286+ 'acceptable_types' : " " .join (sorted (
2287+ self .__accepted_content_type .keys ()))}))
2288+
2289+ if not accept_header :
2290+ # we are using the default
2291+ return (self .__default_accept_type , uri , None )
2292+
2293+ accept_type = ""
2294+ for part in accept_header :
2295+ if accept_type :
2296+ # we accepted the best match, stop searching for
2297+ # lower quality matches.
2298+ break
2299+ if part [0 ] in self .__accepted_content_type :
2300+ accept_type = self .__accepted_content_type [part [0 ]]
2301+ # Version order:
2302+ # 1) accept header version=X specifier
2303+ # application/vnd.x.y; version=1
2304+ # 2) from type in accept-header type/subtype-vX
2305+ # application/vnd.x.y-v1
2306+ # 3) from @apiver in query string to make browser
2307+ # use easy
2308+ # This code handles 1 and 2. Set api_version to none
2309+ # to trigger @apiver parsing below
2310+ # Places that need the api_version info should
2311+ # use default if version = None
2312+ try :
2313+ self .api_version = int (part [1 ]['version' ])
2314+ except KeyError :
2315+ self .api_version = None
2316+ except (ValueError , TypeError ):
2317+ self .client .response_code = 406
2318+ # TypeError if int(None)
2319+ msg = _ ("Unrecognized api version: %s. "
2320+ "See /rest without specifying api version "
2321+ "for supported versions." ) % (
2322+ part [1 ]['version' ])
2323+ return (None , uri ,
2324+ self .error_obj (406 , msg ))
2325+
2326+ # accept_type will be empty only if there is an Accept header
2327+ # with invalid values.
2328+ if accept_type :
2329+ return (accept_type , uri , None )
2330+
2331+ self .client .response_code = 400
2332+ return (None , uri ,
2333+ self .error_obj (
2334+ 406 ,
2335+ _ ("Requested content type(s) '%s' not available.\n "
2336+ "Acceptable mime types are: %s" ) %
2337+ (self .client .request .headers .get ('Accept' ),
2338+ ", " .join (sorted (
2339+ self .__accepted_content_type .keys ())))))
2340+
22092341 def dispatch (self , method , uri , input ):
22102342 """format and process the request"""
22112343 output = None
@@ -2248,75 +2380,15 @@ def dispatch(self, method, uri, input):
22482380 'Ignoring X-HTTP-Method-Override using %s request on %s' ,
22492381 method .upper (), uri )
22502382
2251- # parse Accept header and get the content type
2252- # Acceptable types ordered with preferred one first
2253- # in list.
2254- try :
2255- accept_header = parse_accept_header (headers .get ('Accept' ))
2256- except UsageError as e :
2257- output = self .error_obj (406 , _ ("Unable to parse Accept Header. %(error)s. "
2258- "Acceptable types: %(acceptable_types)s" ) % {
2259- 'error' : e .args [0 ],
2260- 'acceptable_types' : " " .join (sorted (self .__accepted_content_type .keys ()))})
2261- accept_header = []
2262-
2263- if not accept_header :
2264- accept_type = self .__default_accept_type
2265- else :
2266- accept_type = None
2267- for part in accept_header :
2268- if accept_type :
2269- # we accepted the best match, stop searching for
2270- # lower quality matches.
2271- break
2272- if part [0 ] in self .__accepted_content_type :
2273- accept_type = self .__accepted_content_type [part [0 ]]
2274- # Version order:
2275- # 1) accept header version=X specifier
2276- # application/vnd.x.y; version=1
2277- # 2) from type in accept-header type/subtype-vX
2278- # application/vnd.x.y-v1
2279- # 3) from @apiver in query string to make browser
2280- # use easy
2281- # This code handles 1 and 2. Set api_version to none
2282- # to trigger @apiver parsing below
2283- # Places that need the api_version info should
2284- # use default if version = None
2285- try :
2286- self .api_version = int (part [1 ]['version' ])
2287- except KeyError :
2288- self .api_version = None
2289- except (ValueError , TypeError ):
2290- # TypeError if int(None)
2291- msg = ("Unrecognized api version: %s. "
2292- "See /rest without specifying api version "
2293- "for supported versions." % (
2294- part [1 ]['version' ]))
2295- output = self .error_obj (400 , msg )
2296-
2297- # get the request format for response
2298- # priority : extension from uri (/rest/data/issue.json),
2299- # header (Accept: application/json, application/xml)
2300- # default (application/json)
2301- ext_type = os .path .splitext (urlparse (uri ).path )[1 ][1 :]
2383+ # FIXME: when this method is refactored, change
2384+ # determine_output_format to raise an exception. Catch it here
2385+ # and return early. Also set self.client.response_code from
2386+ # error object['error']['status'] and remove from
2387+ # determine_output_format.
2388+ (output_format , uri , error ) = self .determine_output_format (uri )
2389+ if error :
2390+ output = error
23022391
2303- # Check to see if the length of the extension is less than 6.
2304- # this allows use of .vcard for a future use in downloading
2305- # user info. It also allows passing through larger items like
2306- # JWT that has a final component > 6 items. This method also
2307- # allow detection of mistyped types like jon for json.
2308- if ext_type and (len (ext_type ) < 6 ):
2309- # strip extension so uri make sense
2310- # .../issue.json -> .../issue
2311- uri = uri [:- (len (ext_type ) + 1 )]
2312- else :
2313- ext_type = None
2314-
2315- # headers.get('Accept') is never empty if called here.
2316- # accept_type will be set to json if there is no Accept header
2317- # accept_type wil be empty only if there is an Accept header
2318- # with invalid values.
2319- data_type = ext_type or accept_type or headers .get ('Accept' ) or "invalid"
23202392 if method .upper () == 'OPTIONS' :
23212393 # add access-control-allow-* access-control-max-age to support
23222394 # CORS preflight
@@ -2441,14 +2513,17 @@ def dispatch(self, method, uri, input):
24412513 "See /rest without specifying api version "
24422514 "for supported versions." )
24432515 try :
2516+ # FIXME: the version priority here is different
2517+ # from accept header. accept mime type in url
2518+ # takes priority over Accept header. Opposite here.
24442519 if not self .api_version :
24452520 self .api_version = int (input ['@apiver' ].value )
24462521 # Can also return a TypeError ("not indexable")
24472522 # In case the FieldStorage could not parse the result
24482523 except (KeyError , TypeError ):
24492524 self .api_version = None
24502525 except ValueError :
2451- output = self .error_obj (400 , msg % input ['@apiver' ].value )
2526+ output = self .error_obj (406 , msg % input ['@apiver' ].value )
24522527
24532528 # by this time the API version is set. Error if we don't
24542529 # support it?
@@ -2459,7 +2534,7 @@ def dispatch(self, method, uri, input):
24592534 # Use default if not specified for now.
24602535 self .api_version = self .__default_api_version
24612536 elif self .api_version not in self .__supported_api_versions :
2462- output = self .error_obj (400 , msg % self .api_version )
2537+ output = self .error_obj (406 , msg % self .api_version )
24632538
24642539 # sadly del doesn't work on FieldStorage which can be the type of
24652540 # input. So we have to ignore keys starting with @ at other
@@ -2479,19 +2554,23 @@ def dispatch(self, method, uri, input):
24792554 output = self .error_obj (405 , msg .args [0 ])
24802555 self .client .setHeader ("Allow" , msg .args [1 ])
24812556
2482- return self .format_dispatch_output (data_type , output , pretty_output )
2557+ return self .format_dispatch_output (output_format ,
2558+ output ,
2559+ pretty_output )
24832560
24842561 def format_dispatch_output (self , accept_mime_type , output ,
24852562 pretty_print = True ):
24862563 # Format the content type
2487- if accept_mime_type .lower () == "json" :
2564+ # if accept_mime_type is None, the client specified invalid
2565+ # mime types so we default to json output.
2566+ if accept_mime_type == "json" or accept_mime_type is None :
24882567 self .client .setHeader ("Content-Type" , "application/json" )
24892568 if pretty_print :
24902569 indent = 4
24912570 else :
24922571 indent = None
24932572 output = RoundupJSONEncoder (indent = indent ).encode (output )
2494- elif accept_mime_type . lower () == "xml" and dicttoxml :
2573+ elif accept_mime_type == "xml" and dicttoxml :
24952574 self .client .setHeader ("Content-Type" , "application/xml" )
24962575 if 'error' in output :
24972576 # capture values in error with types unsupported
0 commit comments