NbApiClient
A rate-limited, token-refreshing HTTP client for the NationBuilder API, for use in Rails apps that integrate with NationBuilder on behalf of many OAuth-connected accounts ("nations").
It provides two pieces:
NbApiClient::Request— makes a single authenticated API call, transparently retrying on rate limits and refreshing expired OAuth tokens.NbApiClient::RateLimiter— a sliding-window rate limiter, backed by your configuredcache_store(defaults toRails.cache), shared across every process/thread in your fleet, so you don't blow through NationBuilder's server-side rate limit when running many workers. If nocache_storeis configured, rate limiting is disabled entirely.
Installation
This gem is published to rubygems.org. Add to your Gemfile:
gem "nb_api_client"
If you're working against an unreleased checkout instead, a path source still works:
gem "nb_api_client", path: "gems/nb_api_client"
Usage
NbApiClient::Request.call(nation, :get, "/api/v2/people/123")
NbApiClient::Request.call(nation, :post, "/api/v2/people", {person: {email: "a@b.com"}})
The nation interface
nation can be any object — it does not need to be an ActiveRecord model — that responds to:
| Method | Returns |
|---|---|
slug |
A string uniquely identifying the account (used as a cache-key/log prefix). |
active? |
Whether requests should be allowed (checked before every call, except /oauth/token). |
url |
The base URL for the account, e.g. "https://myorg.nationbuilder.com". |
token |
The current OAuth access token, appended as a query param on every request. |
refresh_oauth_token |
Refreshes the OAuth token in place; returns truthy on success, falsy on failure. |
deauthorize |
Called after too many consecutive "unauthorized" responses (see unauthorized_error_threshold below). |
Configuration
NbApiClient.configure do |config|
# Called after `nation.deauthorize` when a nation has failed authorization
# too many times in a row. Use this to notify the account owner or schedule
# a job that retries the OAuth flow later.
config. = ->(nation) {
ReauthorizeNationJob.set(queue: nation.sidekiq_queue).perform_in(1.hour, nation.id)
}
# How many "unauthorized" responses within `unauthorized_error_window`
# seconds before a nation is deauthorized. Defaults: 25 within 300s.
config. = 25
config. = 5 * 60
# Cache store used to count those unauthorized responses, and (below) to
# back the sliding-window rate limiter. Defaults to Rails.cache; must
# respond to #increment(key, amount, options), #decrement, and #read.
config.cache_store = Rails.cache
# Logger for rate-limit/retry warnings. Defaults to Rails.logger.
config.logger = Rails.logger
# If true (the default), Request.call short-circuits to `{}` under
# Rails.env.test? instead of making a real HTTP call. Set to false if you'd
# rather stub HTTP at a lower level (e.g. WebMock/VCR) in your test suite.
config.short_circuit_in_test = true
# Sliding-window rate limit enforced via `cache_store` across every process.
# Disabled entirely if `cache_store` is nil.
config.rate_limit = 200 # requests
config.rate_limit_window_seconds = 10 # per this many seconds
config.rate_limit_max_wait_seconds = 120
end
All of the above have working defaults (matching NationBuilder's published limits with a safety margin), so a new app can start with zero configuration beyond on_repeated_unauthorized.
Error handling
NbApiClient::Request.call raises:
NbApiClient::Request::UnauthorizedNationError— the nation is inactive, or has been deauthorized after repeated auth failures.NbApiClient::Request::RateLimitedError— NationBuilder returned 429 more thanmax_rate_limit_retriestimes in a row.NbApiClient::Request::InvalidContentType— the API returned a non-JSON error body.NbApiClient::RateLimiter::RateLimitExhausted— the local rate limiter couldn't get a slot withinrate_limit_max_wait_seconds.OAuth2::Error— token refresh failed, or the API returned an unrecognized JSON error.
These are ordinary Ruby exception classes, so they compose naturally with e.g. Sidekiq's sidekiq_retry_in/sidekiq_retries_exhausted hooks.
Running the gem's own tests
bundle install
bundle exec rake test
Releasing a new version
- Bump
NbApiClient::VERSIONinlib/nb_api_client/version.rband add an entry toCHANGELOG.md. - Commit, then tag the commit
vX.Y.Zand push the tag. - In the resulting GitLab pipeline, manually run the
publishjob (it refuses to run unless the tag matches the gem version). This requires a protected, maskedGEM_HOST_API_KEYCI/CD variable holding a rubygems.org API key scoped to "push rubygem" for this gem.
License
MIT — see LICENSE.txt.