Skip to content

Commit 4a5fa6b

Browse files
committed
refactor(api): extract api rate limit handling; add default val
Add handle_apiRateLimitExceeded to simplify dispatch() method. Add default value for pretty_print=True to format_dispatch_output.
1 parent aa7d5aa commit 4a5fa6b

File tree

1 file changed

+84
-70
lines changed

1 file changed

+84
-70
lines changed

roundup/rest.py

Lines changed: 84 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,84 @@ def getRateLimit(self):
20372037
# disable rate limiting if either parameter is 0
20382038
return None
20392039

2040+
def handle_apiRateLimitExceeded(self, apiRateLimit):
2041+
"""Determine if the rate limit is exceeded.
2042+
2043+
If not exceeded, return False and the rate limit header values.
2044+
If exceeded, return error message and None
2045+
"""
2046+
gcra = Gcra()
2047+
# unique key is an "ApiLimit-" prefix and the uid)
2048+
apiLimitKey = "ApiLimit-%s" % self.db.getuid()
2049+
otk = self.db.Otk
2050+
try:
2051+
val = otk.getall(apiLimitKey)
2052+
gcra.set_tat_as_string(apiLimitKey, val['tat'])
2053+
except KeyError:
2054+
# ignore if tat not set, it's 1970-1-1 by default.
2055+
pass
2056+
# see if rate limit exceeded and we need to reject the attempt
2057+
reject = gcra.update(apiLimitKey, apiRateLimit)
2058+
2059+
# Calculate a timestamp that will make OTK expire the
2060+
# unused entry 1 hour in the future
2061+
ts = otk.lifetime(3600)
2062+
otk.set(apiLimitKey,
2063+
tat=gcra.get_tat_as_string(apiLimitKey),
2064+
__timestamp=ts)
2065+
otk.commit()
2066+
2067+
limitStatus = gcra.status(apiLimitKey, apiRateLimit)
2068+
if not reject:
2069+
return (False, limitStatus)
2070+
2071+
for header, value in limitStatus.items():
2072+
self.client.setHeader(header, value)
2073+
2074+
# User exceeded limits: tell humans how long to wait
2075+
# Headers above will do the right thing for api
2076+
# aware clients.
2077+
try:
2078+
retry_after = limitStatus['Retry-After']
2079+
except KeyError:
2080+
# handle race condition. If the time between
2081+
# the call to grca.update and grca.status
2082+
# is sufficient to reload the bucket by 1
2083+
# item, Retry-After will be missing from
2084+
# limitStatus. So report a 1 second delay back
2085+
# to the client. We treat update as sole
2086+
# source of truth for exceeded rate limits.
2087+
retry_after = '1'
2088+
self.client.setHeader('Retry-After', retry_after)
2089+
2090+
msg = _("Api rate limits exceeded. Please wait: %s seconds.") % retry_after
2091+
output = self.error_obj(429, msg, source="ApiRateLimiter")
2092+
2093+
# expose these headers to rest clients. Otherwise they can't
2094+
# respond to:
2095+
# rate limiting (*RateLimit*, Retry-After)
2096+
# obsolete API endpoint (Sunset)
2097+
# options request to discover supported methods (Allow)
2098+
self.client.setHeader(
2099+
"Access-Control-Expose-Headers",
2100+
", ".join([
2101+
"X-RateLimit-Limit",
2102+
"X-RateLimit-Remaining",
2103+
"X-RateLimit-Reset",
2104+
"X-RateLimit-Limit-Period",
2105+
"Retry-After",
2106+
"Sunset",
2107+
"Allow",
2108+
])
2109+
)
2110+
2111+
return (self.format_dispatch_output(
2112+
self.__default_accept_type,
2113+
output,
2114+
True # pretty print for this error case as a
2115+
# human may read it
2116+
), None)
2117+
20402118
def dispatch(self, method, uri, input):
20412119
"""format and process the request"""
20422120
output = None
@@ -2048,75 +2126,10 @@ def dispatch(self, method, uri, input):
20482126
apiRateLimit = self.getRateLimit()
20492127

20502128
if apiRateLimit: # if None, disable rate limiting
2051-
gcra = Gcra()
2052-
# unique key is an "ApiLimit-" prefix and the uid)
2053-
apiLimitKey = "ApiLimit-%s" % self.db.getuid()
2054-
otk = self.db.Otk
2055-
try:
2056-
val = otk.getall(apiLimitKey)
2057-
gcra.set_tat_as_string(apiLimitKey, val['tat'])
2058-
except KeyError:
2059-
# ignore if tat not set, it's 1970-1-1 by default.
2060-
pass
2061-
# see if rate limit exceeded and we need to reject the attempt
2062-
reject = gcra.update(apiLimitKey, apiRateLimit)
2063-
2064-
# Calculate a timestamp that will make OTK expire the
2065-
# unused entry 1 hour in the future
2066-
ts = otk.lifetime(3600)
2067-
otk.set(apiLimitKey,
2068-
tat=gcra.get_tat_as_string(apiLimitKey),
2069-
__timestamp=ts)
2070-
otk.commit()
2071-
2072-
limitStatus = gcra.status(apiLimitKey, apiRateLimit)
2073-
if reject:
2074-
for header, value in limitStatus.items():
2075-
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', retry_after)
2091-
2092-
msg = _("Api rate limits exceeded. Please wait: %s seconds.") % retry_after
2093-
output = self.error_obj(429, msg, source="ApiRateLimiter")
2094-
2095-
# expose these headers to rest clients. Otherwise they can't
2096-
# respond to:
2097-
# rate limiting (*RateLimit*, Retry-After)
2098-
# obsolete API endpoint (Sunset)
2099-
# options request to discover supported methods (Allow)
2100-
self.client.setHeader(
2101-
"Access-Control-Expose-Headers",
2102-
", ".join([
2103-
"X-RateLimit-Limit",
2104-
"X-RateLimit-Remaining",
2105-
"X-RateLimit-Reset",
2106-
"X-RateLimit-Limit-Period",
2107-
"Retry-After",
2108-
"Sunset",
2109-
"Allow",
2110-
])
2111-
)
2112-
2113-
return self.format_dispatch_output(
2114-
self.__default_accept_type,
2115-
output,
2116-
True # pretty print for this error case as a
2117-
# human may read it
2118-
)
2119-
2129+
LimitExceeded, limitStatus = self.handle_apiRateLimitExceeded(
2130+
apiRateLimit)
2131+
if LimitExceeded:
2132+
return LimitExceeded # error message
21202133

21212134
for header, value in limitStatus.items():
21222135
# Retry-After will be 0 because
@@ -2373,7 +2386,8 @@ def dispatch(self, method, uri, input):
23732386

23742387
return self.format_dispatch_output(data_type, output, pretty_output)
23752388

2376-
def format_dispatch_output(self, accept_mime_type, output, pretty_print):
2389+
def format_dispatch_output(self, accept_mime_type, output,
2390+
pretty_print=True):
23772391
# Format the content type
23782392
if accept_mime_type.lower() == "json":
23792393
self.client.setHeader("Content-Type", "application/json")

0 commit comments

Comments
 (0)