Skip to content

Commit 285abbd

Browse files
committed
refactor(api): early return if REST rate limit is exceeded
If the rate limit at the top of dispatch() is exceeded, return rather than running to final return at end of method. This does change a couple of things: 1) output format is always in json 2) json is alays pretty printed Previously, the output format requested by the Accept header or format extension in the URL was used for 1. Similarly value of @pretty was used to control 2. I am trying to reduce the complexities in this routine with the goal of fixing: issue2551289 to return 406 error if the Accept header/format extension is incorrect before executing the request.
1 parent 214446c commit 285abbd

File tree

1 file changed

+41
-103
lines changed

1 file changed

+41
-103
lines changed

roundup/rest.py

Lines changed: 41 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,9 +2042,6 @@ def dispatch(self, method, uri, input):
20422042
output = None
20432043

20442044
# Before we do anything has the user hit the rate limit.
2045-
# This should (but doesn't at the moment) bypass
2046-
# all other processing to minimize load of badly
2047-
# behaving client.
20482045

20492046
# Get the limit here and not in the init() routine to allow
20502047
# for a different rate limit per user.
@@ -2076,32 +2073,40 @@ def dispatch(self, method, uri, input):
20762073
if reject:
20772074
for header, value in limitStatus.items():
20782075
self.client.setHeader(header, value)
2079-
# User exceeded limits: tell humans how long to wait
2080-
# Headers above will do the right thing for api
2081-
# aware clients.
2082-
try:
2083-
retry_after = limitStatus['Retry-After']
2084-
except KeyError:
2085-
# handle race condition. If the time between
2086-
# the call to grca.update and grca.status
2087-
# is sufficient to reload the bucket by 1
2088-
# item, Retry-After will be missing from
2089-
# limitStatus. So report a 1 second delay back
2090-
# to the client. We treat update as sole
2091-
# source of truth for exceeded rate limits.
2092-
retry_after = 1
2093-
self.client.setHeader('Retry-After', '1')
2094-
2095-
msg = _("Api rate limits exceeded. Please wait: %s seconds.") % retry_after
2096-
output = self.error_obj(429, msg, source="ApiRateLimiter")
2097-
else:
2098-
for header, value in limitStatus.items():
2099-
# Retry-After will be 0 because
2100-
# user still has quota available.
2101-
# Don't put out the header.
2102-
if header in ('Retry-After',):
2103-
continue
2104-
self.client.setHeader(header, value)
2076+
# User exceeded limits: tell humans how long to wait
2077+
# Headers above will do the right thing for api
2078+
# aware clients.
2079+
try:
2080+
retry_after = limitStatus['Retry-After']
2081+
except KeyError:
2082+
# handle race condition. If the time between
2083+
# the call to grca.update and grca.status
2084+
# is sufficient to reload the bucket by 1
2085+
# item, Retry-After will be missing from
2086+
# limitStatus. So report a 1 second delay back
2087+
# to the client. We treat update as sole
2088+
# source of truth for exceeded rate limits.
2089+
retry_after = 1
2090+
self.client.setHeader('Retry-After', '1')
2091+
2092+
msg = _("Api rate limits exceeded. Please wait: %s seconds.") % retry_after
2093+
output = self.error_obj(429, msg, source="ApiRateLimiter")
2094+
2095+
return self.format_dispatch_output(
2096+
self.__default_accept_type,
2097+
output,
2098+
True # pretty print for this error case as a
2099+
# human may read it
2100+
)
2101+
2102+
2103+
for header, value in limitStatus.items():
2104+
# Retry-After will be 0 because
2105+
# user still has quota available.
2106+
# Don't put out the header.
2107+
if header in ('Retry-After',):
2108+
continue
2109+
self.client.setHeader(header, value)
21052110

21062111
# if X-HTTP-Method-Override is set, follow the override method
21072112
headers = self.client.request.headers
@@ -2121,76 +2126,6 @@ def dispatch(self, method, uri, input):
21212126
'Ignoring X-HTTP-Method-Override using %s request on %s',
21222127
method.upper(), uri)
21232128

2124-
# parse Accept header and get the content type
2125-
# Acceptable types ordered with preferred one first
2126-
# in list.
2127-
try:
2128-
accept_header = parse_accept_header(headers.get('Accept'))
2129-
except UsageError as e:
2130-
output = self.error_obj(406, _("Unable to parse Accept Header. %(error)s. "
2131-
"Acceptable types: %(acceptable_types)s") % {
2132-
'error': e.args[0],
2133-
'acceptable_types': " ".join(sorted(self.__accepted_content_type.keys()))})
2134-
accept_header = []
2135-
2136-
if not accept_header:
2137-
accept_type = self.__default_accept_type
2138-
else:
2139-
accept_type = None
2140-
for part in accept_header:
2141-
if accept_type:
2142-
# we accepted the best match, stop searching for
2143-
# lower quality matches.
2144-
break
2145-
if part[0] in self.__accepted_content_type:
2146-
accept_type = self.__accepted_content_type[part[0]]
2147-
# Version order:
2148-
# 1) accept header version=X specifier
2149-
# application/vnd.x.y; version=1
2150-
# 2) from type in accept-header type/subtype-vX
2151-
# application/vnd.x.y-v1
2152-
# 3) from @apiver in query string to make browser
2153-
# use easy
2154-
# This code handles 1 and 2. Set api_version to none
2155-
# to trigger @apiver parsing below
2156-
# Places that need the api_version info should
2157-
# use default if version = None
2158-
try:
2159-
self.api_version = int(part[1]['version'])
2160-
except KeyError:
2161-
self.api_version = None
2162-
except (ValueError, TypeError):
2163-
# TypeError if int(None)
2164-
msg = ("Unrecognized api version: %s. "
2165-
"See /rest without specifying api version "
2166-
"for supported versions." % (
2167-
part[1]['version']))
2168-
output = self.error_obj(400, msg)
2169-
2170-
# get the request format for response
2171-
# priority : extension from uri (/rest/data/issue.json),
2172-
# header (Accept: application/json, application/xml)
2173-
# default (application/json)
2174-
ext_type = os.path.splitext(urlparse(uri).path)[1][1:]
2175-
2176-
# Check to see if the length of the extension is less than 6.
2177-
# this allows use of .vcard for a future use in downloading
2178-
# user info. It also allows passing through larger items like
2179-
# JWT that has a final component > 6 items. This method also
2180-
# allow detection of mistyped types like jon for json.
2181-
if ext_type and (len(ext_type) < 6):
2182-
# strip extension so uri make sense
2183-
# .../issue.json -> .../issue
2184-
uri = uri[:-(len(ext_type) + 1)]
2185-
else:
2186-
ext_type = None
2187-
2188-
# headers.get('Accept') is never empty if called here.
2189-
# accept_type will be set to json if there is no Accept header
2190-
# accept_type wil be empty only if there is an Accept header
2191-
# with invalid values.
2192-
data_type = ext_type or accept_type or headers.get('Accept') or "invalid"
2193-
21942129
if method.upper() == 'OPTIONS':
21952130
# add access-control-allow-* access-control-max-age to support
21962131
# CORS preflight
@@ -2349,15 +2284,18 @@ def dispatch(self, method, uri, input):
23492284
output = self.error_obj(405, msg.args[0])
23502285
self.client.setHeader("Allow", msg.args[1])
23512286

2287+
return self.format_dispatch_output(data_type, output, pretty_output)
2288+
2289+
def format_dispatch_output(self, accept_mime_type, output, pretty_print):
23522290
# Format the content type
2353-
if data_type.lower() == "json":
2291+
if accept_mime_type.lower() == "json":
23542292
self.client.setHeader("Content-Type", "application/json")
2355-
if pretty_output:
2293+
if pretty_print:
23562294
indent = 4
23572295
else:
23582296
indent = None
23592297
output = RoundupJSONEncoder(indent=indent).encode(output)
2360-
elif data_type.lower() == "xml" and dicttoxml:
2298+
elif accept_mime_type.lower() == "xml" and dicttoxml:
23612299
self.client.setHeader("Content-Type", "application/xml")
23622300
if 'error' in output:
23632301
# capture values in error with types unsupported
@@ -2390,7 +2328,7 @@ def dispatch(self, method, uri, input):
23902328
# display acceptable output.
23912329
self.client.response_code = 406
23922330
output = ("Requested content type '%s' is not available.\n"
2393-
"Acceptable types: %s" % (data_type,
2331+
"Acceptable types: %s" % (accept_mime_type,
23942332
", ".join(sorted(self.__accepted_content_type.keys()))))
23952333

23962334
# Make output json end in a newline to

0 commit comments

Comments
 (0)