-
Notifications
You must be signed in to change notification settings - Fork 137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose rate limiting information #170
Conversation
Hi @philomory thanks for this. We have some internal discussions happening about how we want to handle and expose rate limiting across our various SDKs and want to make sure they are aligned. We'll let you know once we have a decision and if we need any tweaks to fit this PR in with that final design. |
@philomory - While I definitely want to make sure we do this correctly (as in, not make any decisions on this now that we have to back out of later), do you think it would make sense to pass the response headers to all the exceptions and allow for that additional param in
|
That seems like a fairly sensible idea to me. I'm not sure entirely how much utility it has for most exception types, but it's certainly a more flexible implementation than one that is specific to the rate-limiting case, and thus probably more future-proof as well. That said, if you are going to go that route, it may also be worth exposing the HTTP response code along with the headers; some of the exceptions pair 1-to-1 with a specific response code, but in the case of The one caution in both of these cases is that it implies that all At that point it begins to feel like you should just expose the Let me know how you'd like to proceed, and whether you want me to tweak my PR, or whether you want to close this PR and implement the changes yourselves. |
@philomory - Thank you for your thoughtful reply. As you mentioned, those exceptions are not always HTTP-related. Maybe that second parameter could be I like the idea of including the HTTP code as well. Could be just a generic
We'll be replacing RestClient in a future version, likely a major. I do like the idea of an Again, thank you for all this. We're working on a release for this week so if you're able to make these changes, I'd be happy to merge and get this out as part of that. |
So, a basic sketch of what you've outlined above might look like this: class Exception < StandardError
attr_reader :error_code, :additional_information
def initialize(message,error_code=nil,additional_information={})
super(message)
@error_code, @additional_information = error_code, additional_information
end
end
class HTTPError < Auth0::Exception; end
class Unauthorized < Auth0::HTTPError; end
... And get used like this:
In this case the consumer of the exception could get access to the headers via something like: begin
auth0.some_call(args)
rescue Auth0::RateLimitEncountered => e
reset = e.additional_information[:headers]['X-RateLimit-Reset']
wait_until(reset)
retry
end You could add a convenience method to class HTTPError < Auth::Exception
def headers
additional_information[:headers]
end
end ... so it could be called more cleanly, like The only awkward thing about this design is that, for non-HTTP errors that have additional information to provide, but nothing analogous to an HTTP response code, code raising that exception has to explicitly pass raise Auth0::RateLimitEncountered.new(result.body, error_code: result.code, additional_information: { headers: result.headers }) Anyway, with the holiday in the US this week I'm not sure I'll have the time to put together an updated version of the PR this week, not if we also want corresponding unit tests and acceptance tests. |
@philomory - Thanks again for your work here. I totally forgot about the short week so next week is totally fine. I'm not sure if we'll be able to get the rest of the changes in place before then on our end either so no rush. I'd rather wait and get this in. Everything is looking great here. I think the long var name and missing code make a case for just using a single var for all that data we're getting back, like class Exception < StandardError
attr_reader :error_data
def initialize(message,error_data={})
super(message)
@error_data = error_data
end
end
# ...
case result.code
when 200...226 then safe_parse_json result.body
when 400 then Auth0::BadRequest.new result.body, { code: result.code, headers: result.headers }
# ...
class HTTPError < Auth::Exception
def headers
error_data[:headers]
end
def http_code
error_data[:code]
end
end |
I've pushed a patch which adds the changes we discussed in terms of an I also tweaked the new unit tests I had already added, so that they would work with the new setup. I did not add any additional unit tests for the general changes to |
41c8487
to
c8dbd58
Compare
@philomory - Thanks once again! This looks great at first glance. I'm going to take a closer look Monday and get back to you with feedback, if I have any. The tests you've already added look fine, I don't know that integration tests for internals like this would do any more good than the stubbed ones. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of minor things here. Once again, thank you for this, I think it will be a solid improvement to the error handling experience in this lib.
@joshcanhelp @philomory is there anything I can do to unblock this one? Feels like a good, needed contribution to this SDK. Happy to resolve any doubts. |
@philomory - Thanks for the update here! I'm out of office for a bit but I'll pull this down and run through it when I'm back to get it in the next release. |
Apropos of very little, what does the |
@philomory - Indicates a new feature of some kind. We use that label to generate the changelog. I'm pulling this down and testing today/tomorrow and will get this in a release this week 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One possible change and one question for you. I pulled this down locally and everything looks good!
lib/auth0/exception.rb
Outdated
# Invalid parameter passed, e.g. empty where ID is required | ||
class InvalidParameter < Auth0::Exception; end | ||
# Invalid Auth0 credentials either client_id/secret for API v1 | ||
# or JWT for API v2/ | ||
class InvalidCredentials < Auth0::Exception; end | ||
# Invalid Auth0 API namespace | ||
class InvalidApiNamespace < Auth0::Exception; end | ||
# Auth0 API rate-limiting encountered | ||
class RateLimitEncountered < Auth0::HTTPError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we extend Auth0::Unsupported
here instead of Auth0::HTTPError
? Without that, we're not backwards-compatible (we're now throwing a different exception and, for apps that are rescuing Auth0::Unsupported
in the case of a rate limit hit, that exception will bubble up).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, hm, interesting point. From an API-compatibility point, yes; from a semantic standpoint, no?
Actually, I wonder if Auth0::Unsupported
should just become an alias for Auth0::HTTPError
. After all, what is Auth0::Unsupported
if not "An HTTP error that this gem doesn't have any more specific classification for"?
E.g., instead of making Auth0::RateLimitEncountered
inherit from Auth0::Unsupported
, just have this line in there:
Unsupported = Auth0::HTTPError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. Actually, maybe that's not great either, since it would broaden the scope of Auth0::Unsupported
; rescue Auth0::Unsupported
would now rescue error types that it didn't previously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm more worried about breaking folks on a minor than semantics, at this point. We're due for a major on this SDK (a number of deprecated methods and addressing the HTTP client we're using) and could remove that extension then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've made Auth0::RateLimitEncountered
a subclass of Auth0::Unsupported
, with a comment explaining the reason and a TODO item to move it at a major version change.
The snyk CI check is now failing, although from what I can tell (I'm not very familiar with snyk) it seems to be unrelated to this PR - if I'm reading correctly, a vulnerability apparently exists in YARD, which this gem has as a development dependency. Specifically, a new vulnerability seems to have been published a few days ago. |
@philomory - Deps fixed in |
5dcafe0
to
f0e53a3
Compare
All checks now pass 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@philomory - Thanks once again for this great addition and sticking through all the changes 🙇
I noticed that we had type error when we were trying to get the `.reset` time in an RateLimitEncountered. Headers are symbolized and are not strings. To reproduce de behavior I did ```ruby t = [] 85.times do t << Thread.new do auth0_client.clients rescue Auth0::RateLimitEncountered => e puts <<~DOC exception: #{e.inspect} reset: #{e.reset}\n" DOC end end end; nil p t.map(&:join); nil p 1 ``` Without the fix ``` .../gems/activesupport-6.0.3.6/lib/active_support/core_ext/time/calculations.rb:53:in `at': can't convert nil into an exact number (TypeError) ``` Inspecting headers: ``` headers['X-RateLimit-Reset']: nil, headers[:x_ratelimit_reset]: 1619192310 ``` With the fix: ``` exception: #<Auth0::RateLimitEncountered: {"statusCode":429,"error":"Too Many Requests","message":"Global limit has been reached","errorCode":"too_many_requests"}> reset: 2021-04-23 15:49:17 UTC ``` Related: - auth0#170
…272) I noticed that we had type error when we were trying to get the `.reset` time in an RateLimitEncountered. Headers are symbolized and are not strings. To reproduce de behavior I did ```ruby t = [] 85.times do t << Thread.new do auth0_client.clients rescue Auth0::RateLimitEncountered => e puts <<~DOC exception: #{e.inspect} reset: #{e.reset}\n" DOC end end end; nil p t.map(&:join); nil p 1 ``` Without the fix ``` .../gems/activesupport-6.0.3.6/lib/active_support/core_ext/time/calculations.rb:53:in `at': can't convert nil into an exact number (TypeError) ``` Inspecting headers: ``` headers['X-RateLimit-Reset']: nil, headers[:x_ratelimit_reset]: 1619192310 ``` With the fix: ``` exception: #<Auth0::RateLimitEncountered: {"statusCode":429,"error":"Too Many Requests","message":"Global limit has been reached","errorCode":"too_many_requests"}> reset: 2021-04-23 15:49:17 UTC ``` Related: - #170
Changes
This PR adds a new exception class, Auth0::RateLimitEncountered. In addition to the message and backtrace, this exception has attributes to hold the values of the Auth0 Rate Limiting Headers.
Thus, you can get access to this information when you encountered rate limiting, for example:
References
This pull request address #158
A future improvement might incorporate (optional) automatic retry-after-wait into the gem itself, but for now exposing the required information to library users is enough.
Testing
This test adds unit tests, but not integration tests.
Checklist