@@ -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