Files
shields/services/librariesio/librariesio-api-provider.js
Caleb Cartwright ae58e4a211 Add authentication for Libraries.io-based badges, run [Libraries Bower] (#7080)
* feat: support authentication on Libraries.io requests

* feat: wire up libraries.io config and api provider instantiation

* feat: create libraries.io and bower base classes

* refactor: tweak libraries/bower service classes and tests

* rename request fetcher function/arg

* throw exception when no tokens available

* cleanup old value

Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2021-10-28 19:21:24 -05:00

109 lines
3.6 KiB
JavaScript

import { ImproperlyConfigured } from '../index.js'
import log from '../../core/server/log.js'
import { TokenPool } from '../../core/token-pooling/token-pool.js'
import { userAgent } from '../../core/base-service/legacy-request-handler.js'
// Provides an interface to the Libraries.io API.
export default class LibrariesIoApiProvider {
constructor({ baseUrl, tokens = [], defaultRateLimit = 60 }) {
const withPooling = tokens.length > 1
Object.assign(this, {
baseUrl,
withPooling,
globalToken: tokens[0],
defaultRateLimit,
})
if (this.withPooling) {
this.standardTokens = new TokenPool({ batchSize: 45 })
tokens.forEach(t => this.standardTokens.add(t, {}, defaultRateLimit))
}
}
getRateLimitFromHeaders({ headers, token }) {
// The Libraries.io API does not consistently provide the rate limiting headers.
// In some cases (e.g. package/version not founds) it won't include any of these headers,
// and the `retry-after` header is only provided _after_ the rate limit has been exceeded
// and requests are throttled.
//
// https://github.com/librariesio/libraries.io/issues/2860
// The standard rate limit is 60/requests/minute, so fallback to that default
// if the header isn't present.
// https://libraries.io/api#rate-limit
const rateLimit = headers['x-ratelimit-limit'] || this.defaultRateLimit
// If the remaining header is missing, then we're in the 404 response phase, and simply
// subtract one from the `usesRemaining` count on the token, since the 404 responses do count
// against the rate limits.
const totalUsesRemaining =
headers['x-ratelimit-remaining'] || token.decrementedUsesRemaining
// The `retry-after` header is only present post-rate limit excess, and contains the value in
// seconds the client needs to wait before the limits are reset.
// Our token pools internally use UTC-based milliseconds, so we perform the conversion
// if the header is present to ensure the token pool has the correct value.
// If the header is absent, we just use the current timestamp to
// advance the value to _something_
const retryAfter = headers['retry-after']
const nextReset = Date.now() + (retryAfter ? retryAfter * 1000 : 0)
return {
rateLimit,
totalUsesRemaining,
nextReset,
}
}
updateToken({ token, res }) {
const { totalUsesRemaining, nextReset } = this.getRateLimitFromHeaders({
headers: res.headers,
token,
})
token.update(totalUsesRemaining, nextReset)
}
async fetch(requestFetcher, url, options = {}) {
const { baseUrl } = this
let token
let tokenString
if (this.withPooling) {
try {
token = this.standardTokens.next()
} catch (e) {
log.error(e)
throw new ImproperlyConfigured({
prettyMessage: 'Unable to select next Libraries.io token from pool',
})
}
tokenString = token.id
} else {
tokenString = this.globalToken
}
const mergedOptions = {
...options,
...{
headers: {
'User-Agent': userAgent,
...options.headers,
},
qs: {
api_key: tokenString,
...options.qs,
},
},
}
const response = await requestFetcher(`${baseUrl}${url}`, mergedOptions)
if (this.withPooling) {
if (response.res.statusCode === 401) {
this.invalidateToken(token)
} else if (response.res.statusCode < 500) {
this.updateToken({ token, url, res: response.res })
}
}
return response
}
}