@jupiterone/integration-sdk-http-client 12.1.0 → 12.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ import { Response } from 'node-fetch';
2
+ import { IntegrationLogger } from '@jupiterone/integration-sdk-core';
3
+ import { AttemptContext } from '@lifeomic/attempt';
4
+ import { ClientConfig, OptionalPromise, RetryOptions, RequestOptions, RateLimitThrottlingOptions, IterateCallbackResult, TokenBucketOptions, NextPageData } from './types';
5
+ import { HierarchicalTokenBucket } from '@jupiterone/hierarchical-token-bucket';
6
+ export declare const defaultErrorHandler: (err: any, context: AttemptContext, logger: IntegrationLogger) => Promise<void>;
7
+ export declare abstract class BaseAPIClient {
8
+ protected baseUrl: string;
9
+ protected logger: IntegrationLogger;
10
+ protected retryOptions: RetryOptions;
11
+ protected logErrorBody: boolean;
12
+ protected rateLimitThrottling: RateLimitThrottlingOptions | undefined;
13
+ protected baseTokenBucket?: HierarchicalTokenBucket;
14
+ protected tokenBucketInitialConfig?: TokenBucketOptions | undefined;
15
+ protected endpointTokenBuckets: Record<string, HierarchicalTokenBucket>;
16
+ /**
17
+ * The authorization headers for the API requests
18
+ */
19
+ protected authorizationHeaders: Record<string, string>;
20
+ /**
21
+ * Create a new API client
22
+ *
23
+ * @param {ClientConfig} config - The configuration for the client
24
+ * @param {string} config.baseUrl - The base URL for the API
25
+ * @param {IntegrationLogger} config.logger - The logger to use
26
+ * @param {Partial<RetryOptions>} [config.retryOptions] - The retry options for the client,
27
+ * if not provided, the default retry options will be used
28
+ * @param {boolean} [config.logErrorBody] - Whether to log the error body,
29
+ * if true, the error body will be logged when a request fails
30
+ * @param {RateLimitThrottlingOptions} [config.rateLimitThrottling] - The rate limit throttling options,
31
+ * if not provided, rate limit throttling will not be enabled
32
+ * @param {number} [config.rateLimitThrottling.threshold] - The threshold at which to throttle requests
33
+ * @param {RateLimitHeaders} [config.rateLimitThrottling.rateLimitHeaders] - The headers to use for rate limiting
34
+ * @param {string} [config.rateLimitThrottling.rateLimitHeaders.limit='x-rate-limit-limit'] - The header for the rate limit limit
35
+ * @param {string} [config.rateLimitThrottling.rateLimitHeaders.remaining='x-rate-limit-remaining'] - The header for the rate limit remaining
36
+ * @param {string} [config.rateLimitThrottling.rateLimitHeaders.reset='x-rate-limit-reset'] - The header for the rate limit reset
37
+ * @param {TokenBucketOptions} [config.tokenBucket] - The token bucket options,
38
+ * if not provided, token bucket will not be enabled
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const client = new APIClient({
43
+ * baseUrl: 'https://api.example.com',
44
+ * logger: context.logger,
45
+ * rateLimitThrottling: {
46
+ * threshold: 0.3,
47
+ * rateLimitHeaders: {
48
+ * limit: 'x-ratelimit-limit',
49
+ * remaining: 'x-ratelimit-remaining',
50
+ * reset: 'x-ratelimit-reset',
51
+ * },
52
+ * },
53
+ * })
54
+ * ```
55
+ */
56
+ constructor(config: ClientConfig);
57
+ protected withBaseUrl(endpoint: string): string;
58
+ /**
59
+ * Get the authorization headers for the API requests.
60
+ *
61
+ * @return {Promise<Record<string, string>>} - The authorization headers
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * async getAuthorizationHeaders(): Record<string, string> {
66
+ * return {
67
+ * Authorization: `Bearer ${this.config.apiKey}`,
68
+ * };
69
+ * }
70
+ * ```
71
+ * @example
72
+ * ```typescript
73
+ * protected async getAuthorizationHeaders(): Promise<Record<string, string>> {
74
+ * const response = await this.request('/token', {
75
+ * method: 'POST',
76
+ * body: {
77
+ * email: this.config.email,
78
+ * password: this.config.password,
79
+ * },
80
+ * authorize: false, // don't try to do authorization on this request, it will go into an infinite loop.
81
+ * });
82
+ * const data = await response.json();
83
+ * return {
84
+ * Authorization: `Bearer ${data.token}`,
85
+ * };
86
+ * }
87
+ */
88
+ protected abstract getAuthorizationHeaders(): OptionalPromise<Record<string, string>>;
89
+ /**
90
+ * Perform a request to the API.
91
+ *
92
+ * @param {string} endpoint - The endpoint to request
93
+ * @param {RequestOptions} options - The options for the request
94
+ * @param {string} [options.method=GET] - The HTTP method to use
95
+ * @param {Record<string, unknown>} [options.body] - The body of the request
96
+ * @param {Record<string, string>} [options.headers] - The headers for the request
97
+ * @param {boolean} [options.authorize=true] - Whether to include authorization headers
98
+ * @return {Promise<Response>} - The response from the API
99
+ */
100
+ protected request(endpoint: string, options?: RequestOptions): Promise<Response>;
101
+ /**
102
+ * Perform a request to the API with retries and error handling.
103
+ *
104
+ * @param {string} endpoint - The endpoint path to request
105
+ * @param {RequestOptions} options - The options for the request
106
+ * @param {string} [options.method=GET] - The HTTP method to use
107
+ * @param {Record<string, unknown>} [options.body] - The body of the request
108
+ * @param {Record<string, string>} [options.headers] - The headers for the request
109
+ * @param {boolean} [options.authorize=true] - Whether to include authorization headers
110
+ * @return {Promise<Response>} - The response from the API
111
+ */
112
+ protected retryableRequest(endpoint: string, options?: RequestOptions): Promise<Response>;
113
+ /**
114
+ * Iteratively performs API requests based on the initial request and subsequent requests defined by a callback function.
115
+ * This method is designed to facilitate paginated API requests where each request's response determines the parameters of the next request.
116
+ *
117
+ * @param {object} initialRequest - The initial request parameters
118
+ * @param {string} initialRequest.endpoint - The endpoint path to request
119
+ * @param {RequestOptions} [initialRequest.options] - The options for the request
120
+ * @param {string|string[]|undefined} dataPath - The path to the data in the response to iterate over
121
+ * @param {function} nextPageCallback - The callback function to determine the parameters of the next request
122
+ *
123
+ * @return {AsyncGenerator<T, void, unknown>} - An async generator that yields the items from the paginated requests
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * async iterateUsers(iteratee: (user: User) => Promise<void>): Promise<void>{
128
+ * const baseEndpoint = '/users?limit=100';
129
+ * const iterator = this.paginate({
130
+ * endpoint: baseEndpoint,
131
+ * }, 'users', this.getNextPageCallback(baseEndpoint));
132
+ *
133
+ * for await (const user of iterator) {
134
+ * await iteratee(user);
135
+ * }
136
+ * };
137
+ *
138
+ * private getNextPageCallback(baseEndpoint: string) {
139
+ * return (data: NextPageData): IterateCallbackResult | undefined => {
140
+ * const { body } = data;
141
+ * const nextCursor = body.metadata?.cursor;
142
+ * if (!nextCursor) {
143
+ * return;
144
+ * }
145
+ * const nextUrl = `${baseEndpoint}&cursor=${nextCursor}`;
146
+ * return {
147
+ * nextUrl,
148
+ * };
149
+ * }
150
+ * };
151
+ * ```
152
+ */
153
+ protected paginate<T>(initialRequest: {
154
+ endpoint: string;
155
+ options?: RequestOptions;
156
+ }, dataPath: string | string[] | undefined, nextPageCallback: (data: NextPageData) => IterateCallbackResult | undefined): AsyncGenerator<T, void, unknown>;
157
+ /**
158
+ * Get the token bucket for the given endpoint
159
+ *
160
+ * @param {string} endpoint - The endpoint to get the token bucket for
161
+ * @param {number} [tokens] - The number of tokens to use for the token bucket
162
+ */
163
+ private getTokenBucket;
164
+ /**
165
+ * Wait until the rate limit reset time before sending the next request
166
+ * if the rate limit threshold is exceeded.
167
+ *
168
+ * @param {function} fn - The function to rate limit
169
+ * @return {Promise<Response>}
170
+ */
171
+ protected withRateLimiting(fn: () => Promise<Response>): Promise<Response>;
172
+ private parseRateLimitHeaders;
173
+ /**
174
+ * Determine if the next request should be throttled based on the rate limit headers
175
+ *
176
+ * @param {object} params - The parameters for the function
177
+ * @param {number|undefined} params.rateLimitLimit - The rate limit limit
178
+ * @param {number|undefined} params.rateLimitRemaining - The rate limit remaining
179
+ * @param {number} params.threshold - The threshold at which to throttle requests
180
+ * @return {boolean} - Whether the next request should be throttled
181
+ */
182
+ private shouldThrottleNextRequest;
183
+ /**
184
+ * Determine wait time by getting the delta X-Rate-Limit-Reset and the Date header
185
+ * Add 1 second to account for sub second differences between the clocks that create these headers
186
+ */
187
+ private getRetryDelayMs;
188
+ private getRateLimitHeaderValue;
189
+ }
@@ -0,0 +1,370 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BaseAPIClient = exports.defaultErrorHandler = void 0;
7
+ const node_fetch_1 = __importDefault(require("node-fetch"));
8
+ const attempt_1 = require("@lifeomic/attempt");
9
+ const errors_1 = require("./errors");
10
+ const get_1 = __importDefault(require("lodash/get"));
11
+ const hierarchical_token_bucket_1 = require("@jupiterone/hierarchical-token-bucket");
12
+ const defaultErrorHandler = async (err, context, logger) => {
13
+ if (err.code === 'ECONNRESET' || err.message.includes('ECONNRESET')) {
14
+ return;
15
+ }
16
+ if (!err.retryable) {
17
+ // can't retry this? just abort
18
+ context.abort();
19
+ return;
20
+ }
21
+ if (err.status === 429) {
22
+ const retryAfter = err.retryAfter ? err.retryAfter * 1000 : 5000;
23
+ logger.warn({ retryAfter }, 'Received a rate limit error. Waiting before retrying.');
24
+ await (0, attempt_1.sleep)(retryAfter);
25
+ }
26
+ };
27
+ exports.defaultErrorHandler = defaultErrorHandler;
28
+ const DEFAULT_RETRY_OPTIONS = {
29
+ maxAttempts: 3,
30
+ delay: 30000,
31
+ timeout: 180000,
32
+ factor: 2,
33
+ handleError: exports.defaultErrorHandler,
34
+ };
35
+ const DEFAULT_RATE_LIMIT_HEADERS = {
36
+ limit: 'x-rate-limit-limit',
37
+ remaining: 'x-rate-limit-remaining',
38
+ reset: 'x-rate-limit-reset',
39
+ };
40
+ class BaseAPIClient {
41
+ baseUrl;
42
+ logger;
43
+ retryOptions;
44
+ logErrorBody;
45
+ rateLimitThrottling;
46
+ baseTokenBucket;
47
+ tokenBucketInitialConfig;
48
+ endpointTokenBuckets = {};
49
+ /**
50
+ * The authorization headers for the API requests
51
+ */
52
+ authorizationHeaders;
53
+ /**
54
+ * Create a new API client
55
+ *
56
+ * @param {ClientConfig} config - The configuration for the client
57
+ * @param {string} config.baseUrl - The base URL for the API
58
+ * @param {IntegrationLogger} config.logger - The logger to use
59
+ * @param {Partial<RetryOptions>} [config.retryOptions] - The retry options for the client,
60
+ * if not provided, the default retry options will be used
61
+ * @param {boolean} [config.logErrorBody] - Whether to log the error body,
62
+ * if true, the error body will be logged when a request fails
63
+ * @param {RateLimitThrottlingOptions} [config.rateLimitThrottling] - The rate limit throttling options,
64
+ * if not provided, rate limit throttling will not be enabled
65
+ * @param {number} [config.rateLimitThrottling.threshold] - The threshold at which to throttle requests
66
+ * @param {RateLimitHeaders} [config.rateLimitThrottling.rateLimitHeaders] - The headers to use for rate limiting
67
+ * @param {string} [config.rateLimitThrottling.rateLimitHeaders.limit='x-rate-limit-limit'] - The header for the rate limit limit
68
+ * @param {string} [config.rateLimitThrottling.rateLimitHeaders.remaining='x-rate-limit-remaining'] - The header for the rate limit remaining
69
+ * @param {string} [config.rateLimitThrottling.rateLimitHeaders.reset='x-rate-limit-reset'] - The header for the rate limit reset
70
+ * @param {TokenBucketOptions} [config.tokenBucket] - The token bucket options,
71
+ * if not provided, token bucket will not be enabled
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * const client = new APIClient({
76
+ * baseUrl: 'https://api.example.com',
77
+ * logger: context.logger,
78
+ * rateLimitThrottling: {
79
+ * threshold: 0.3,
80
+ * rateLimitHeaders: {
81
+ * limit: 'x-ratelimit-limit',
82
+ * remaining: 'x-ratelimit-remaining',
83
+ * reset: 'x-ratelimit-reset',
84
+ * },
85
+ * },
86
+ * })
87
+ * ```
88
+ */
89
+ constructor(config) {
90
+ this.baseUrl = config.baseUrl;
91
+ this.logger = config.logger;
92
+ this.retryOptions = {
93
+ ...DEFAULT_RETRY_OPTIONS,
94
+ ...config.retryOptions,
95
+ };
96
+ this.logErrorBody = config.logErrorBody ?? false;
97
+ this.rateLimitThrottling = config.rateLimitThrottling;
98
+ if (config.tokenBucket) {
99
+ this.tokenBucketInitialConfig = config.tokenBucket;
100
+ this.baseTokenBucket = new hierarchical_token_bucket_1.HierarchicalTokenBucket({
101
+ maximumCapacity: config.tokenBucket.maximumCapacity,
102
+ refillRate: config.tokenBucket.refillRate,
103
+ });
104
+ }
105
+ }
106
+ withBaseUrl(endpoint) {
107
+ return new URL(endpoint, this.baseUrl).toString();
108
+ }
109
+ /**
110
+ * Perform a request to the API.
111
+ *
112
+ * @param {string} endpoint - The endpoint to request
113
+ * @param {RequestOptions} options - The options for the request
114
+ * @param {string} [options.method=GET] - The HTTP method to use
115
+ * @param {Record<string, unknown>} [options.body] - The body of the request
116
+ * @param {Record<string, string>} [options.headers] - The headers for the request
117
+ * @param {boolean} [options.authorize=true] - Whether to include authorization headers
118
+ * @return {Promise<Response>} - The response from the API
119
+ */
120
+ async request(endpoint, options) {
121
+ const tokenBucket = this.getTokenBucket(endpoint, options?.bucketTokens);
122
+ if (tokenBucket) {
123
+ const timeToWaitInMs = tokenBucket.take();
124
+ await (0, attempt_1.sleep)(timeToWaitInMs);
125
+ }
126
+ const { method = 'GET', body, headers, authorize = true } = options ?? {};
127
+ if (authorize && !this.authorizationHeaders) {
128
+ this.authorizationHeaders = await this.getAuthorizationHeaders();
129
+ }
130
+ let url;
131
+ try {
132
+ url = new URL(endpoint).toString();
133
+ }
134
+ catch (e) {
135
+ // If the path is not a valid URL, assume it's a path and prepend the base URL
136
+ url = this.withBaseUrl(endpoint);
137
+ }
138
+ const response = await (0, node_fetch_1.default)(url, {
139
+ method,
140
+ headers: {
141
+ 'Content-Type': 'application/json',
142
+ Accept: 'application/json',
143
+ ...(authorize && this.authorizationHeaders),
144
+ ...headers,
145
+ },
146
+ body: body ? JSON.stringify(body) : undefined,
147
+ });
148
+ return response;
149
+ }
150
+ /**
151
+ * Perform a request to the API with retries and error handling.
152
+ *
153
+ * @param {string} endpoint - The endpoint path to request
154
+ * @param {RequestOptions} options - The options for the request
155
+ * @param {string} [options.method=GET] - The HTTP method to use
156
+ * @param {Record<string, unknown>} [options.body] - The body of the request
157
+ * @param {Record<string, string>} [options.headers] - The headers for the request
158
+ * @param {boolean} [options.authorize=true] - Whether to include authorization headers
159
+ * @return {Promise<Response>} - The response from the API
160
+ */
161
+ async retryableRequest(endpoint, options) {
162
+ const { method = 'GET', body, headers, authorize = true } = options ?? {};
163
+ return (0, attempt_1.retry)(async () => {
164
+ return this.withRateLimiting(async () => {
165
+ let response;
166
+ try {
167
+ response = await this.request(endpoint, {
168
+ method,
169
+ body,
170
+ headers,
171
+ authorize,
172
+ });
173
+ }
174
+ catch (err) {
175
+ this.logger.error({ code: err.code, err, endpoint }, 'Error sending request');
176
+ throw err;
177
+ }
178
+ if (response.ok) {
179
+ return response;
180
+ }
181
+ let error;
182
+ const requestErrorParams = {
183
+ endpoint,
184
+ response,
185
+ logger: this.logger,
186
+ logErrorBody: this.logErrorBody,
187
+ };
188
+ if ((0, errors_1.isRetryableRequest)(response.status)) {
189
+ error = await (0, errors_1.retryableRequestError)(requestErrorParams);
190
+ }
191
+ else {
192
+ error = await (0, errors_1.fatalRequestError)(requestErrorParams);
193
+ }
194
+ for await (const _chunk of response.body) {
195
+ // force consumption of body to avoid memory leaks
196
+ // https://github.com/node-fetch/node-fetch/issues/83
197
+ }
198
+ throw error;
199
+ });
200
+ }, {
201
+ maxAttempts: this.retryOptions.maxAttempts,
202
+ delay: this.retryOptions.delay,
203
+ timeout: this.retryOptions.timeout,
204
+ factor: this.retryOptions.factor,
205
+ handleError: async (err, context) => {
206
+ await this.retryOptions.handleError(err, context, this.logger);
207
+ },
208
+ });
209
+ }
210
+ /**
211
+ * Iteratively performs API requests based on the initial request and subsequent requests defined by a callback function.
212
+ * This method is designed to facilitate paginated API requests where each request's response determines the parameters of the next request.
213
+ *
214
+ * @param {object} initialRequest - The initial request parameters
215
+ * @param {string} initialRequest.endpoint - The endpoint path to request
216
+ * @param {RequestOptions} [initialRequest.options] - The options for the request
217
+ * @param {string|string[]|undefined} dataPath - The path to the data in the response to iterate over
218
+ * @param {function} nextPageCallback - The callback function to determine the parameters of the next request
219
+ *
220
+ * @return {AsyncGenerator<T, void, unknown>} - An async generator that yields the items from the paginated requests
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * async iterateUsers(iteratee: (user: User) => Promise<void>): Promise<void>{
225
+ * const baseEndpoint = '/users?limit=100';
226
+ * const iterator = this.paginate({
227
+ * endpoint: baseEndpoint,
228
+ * }, 'users', this.getNextPageCallback(baseEndpoint));
229
+ *
230
+ * for await (const user of iterator) {
231
+ * await iteratee(user);
232
+ * }
233
+ * };
234
+ *
235
+ * private getNextPageCallback(baseEndpoint: string) {
236
+ * return (data: NextPageData): IterateCallbackResult | undefined => {
237
+ * const { body } = data;
238
+ * const nextCursor = body.metadata?.cursor;
239
+ * if (!nextCursor) {
240
+ * return;
241
+ * }
242
+ * const nextUrl = `${baseEndpoint}&cursor=${nextCursor}`;
243
+ * return {
244
+ * nextUrl,
245
+ * };
246
+ * }
247
+ * };
248
+ * ```
249
+ */
250
+ async *paginate(initialRequest, dataPath, nextPageCallback) {
251
+ let nextUrl;
252
+ let nextRequestOptions;
253
+ let isInitialRequest = true;
254
+ do {
255
+ const response = await this.retryableRequest(isInitialRequest ? initialRequest.endpoint : nextUrl, isInitialRequest ? initialRequest.options : nextRequestOptions);
256
+ if (response.status === 204) {
257
+ break;
258
+ }
259
+ const data = await response.json();
260
+ let items = dataPath ? (0, get_1.default)(data, dataPath) : data;
261
+ items = Array.isArray(items) ? items : [];
262
+ for (const item of items) {
263
+ yield item;
264
+ }
265
+ const cbOptions = nextPageCallback({
266
+ body: data,
267
+ headers: response.headers.raw(),
268
+ });
269
+ nextUrl = cbOptions?.nextUrl;
270
+ nextRequestOptions = cbOptions?.nextRequestOptions;
271
+ isInitialRequest = false;
272
+ } while (nextUrl);
273
+ }
274
+ /**
275
+ * Get the token bucket for the given endpoint
276
+ *
277
+ * @param {string} endpoint - The endpoint to get the token bucket for
278
+ * @param {number} [tokens] - The number of tokens to use for the token bucket
279
+ */
280
+ getTokenBucket(endpoint, tokens) {
281
+ if (!this.baseTokenBucket) {
282
+ return;
283
+ }
284
+ const path = endpoint.split('?')[0];
285
+ if (this.endpointTokenBuckets[path]) {
286
+ return this.endpointTokenBuckets[path];
287
+ }
288
+ this.endpointTokenBuckets[path] = new hierarchical_token_bucket_1.HierarchicalTokenBucket({
289
+ parent: this.baseTokenBucket,
290
+ maximumCapacity: tokens ?? this.tokenBucketInitialConfig?.maximumCapacity ?? 10000,
291
+ refillRate: tokens ?? this.tokenBucketInitialConfig?.refillRate ?? 10000,
292
+ });
293
+ return this.endpointTokenBuckets[path];
294
+ }
295
+ /**
296
+ * Wait until the rate limit reset time before sending the next request
297
+ * if the rate limit threshold is exceeded.
298
+ *
299
+ * @param {function} fn - The function to rate limit
300
+ * @return {Promise<Response>}
301
+ */
302
+ async withRateLimiting(fn) {
303
+ const response = await fn();
304
+ if (!this.rateLimitThrottling) {
305
+ return response;
306
+ }
307
+ const { rateLimitLimit, rateLimitRemaining } = this.parseRateLimitHeaders(response.headers);
308
+ if (this.shouldThrottleNextRequest({
309
+ rateLimitLimit,
310
+ rateLimitRemaining,
311
+ threshold: this.rateLimitThrottling.threshold,
312
+ })) {
313
+ const timeToSleepInMs = this.getRetryDelayMs(response.headers);
314
+ const thresholdPercentage = this.rateLimitThrottling.threshold * 100;
315
+ const resetHeaderName = this.rateLimitThrottling.rateLimitHeaders?.reset ??
316
+ 'x-rate-limit-reset';
317
+ this.logger.warn({ rateLimitLimit, rateLimitRemaining, timeToSleepInMs }, `Exceeded ${thresholdPercentage}% of rate limit. Sleeping until ${resetHeaderName}`);
318
+ await (0, attempt_1.sleep)(timeToSleepInMs);
319
+ }
320
+ return response;
321
+ }
322
+ parseRateLimitHeaders(headers) {
323
+ const strRateLimitLimit = this.getRateLimitHeaderValue(headers, 'limit');
324
+ const strRateLimitRemaining = this.getRateLimitHeaderValue(headers, 'remaining');
325
+ return {
326
+ rateLimitLimit: strRateLimitLimit
327
+ ? parseInt(strRateLimitLimit, 10)
328
+ : undefined,
329
+ rateLimitRemaining: strRateLimitRemaining
330
+ ? parseInt(strRateLimitRemaining, 10)
331
+ : undefined,
332
+ };
333
+ }
334
+ /**
335
+ * Determine if the next request should be throttled based on the rate limit headers
336
+ *
337
+ * @param {object} params - The parameters for the function
338
+ * @param {number|undefined} params.rateLimitLimit - The rate limit limit
339
+ * @param {number|undefined} params.rateLimitRemaining - The rate limit remaining
340
+ * @param {number} params.threshold - The threshold at which to throttle requests
341
+ * @return {boolean} - Whether the next request should be throttled
342
+ */
343
+ shouldThrottleNextRequest(params) {
344
+ const { rateLimitLimit, rateLimitRemaining } = params;
345
+ if (rateLimitLimit === undefined || rateLimitRemaining === undefined)
346
+ return false;
347
+ const rateLimitConsumed = rateLimitLimit - rateLimitRemaining;
348
+ return rateLimitConsumed / rateLimitLimit > params.threshold;
349
+ }
350
+ /**
351
+ * Determine wait time by getting the delta X-Rate-Limit-Reset and the Date header
352
+ * Add 1 second to account for sub second differences between the clocks that create these headers
353
+ */
354
+ getRetryDelayMs(headers) {
355
+ const resetValue = this.getRateLimitHeaderValue(headers, 'reset');
356
+ if (!resetValue) {
357
+ // If the header is not present, we can't determine the wait time, so we'll just wait 60 seconds
358
+ return 60000;
359
+ }
360
+ const nowDate = new Date(headers.get('date') ?? Date.now());
361
+ const retryDate = new Date(parseInt(resetValue, 10) * 1000);
362
+ return retryDate.getTime() - nowDate.getTime() + 1000;
363
+ }
364
+ getRateLimitHeaderValue(headers, header) {
365
+ return headers.get(this.rateLimitThrottling?.rateLimitHeaders?.[header] ??
366
+ DEFAULT_RATE_LIMIT_HEADERS[header]);
367
+ }
368
+ }
369
+ exports.BaseAPIClient = BaseAPIClient;
370
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,30 @@
1
- export interface APIErrorParams {
2
- message: string;
3
- code: number;
1
+ import { IntegrationLogger, IntegrationProviderAPIError } from '@jupiterone/integration-sdk-core';
2
+ import { Response } from 'node-fetch';
3
+ type RateLimitErrorParams = ConstructorParameters<typeof IntegrationProviderAPIError>[0] & {
4
+ retryAfter: number;
5
+ };
6
+ interface RequestErrorParams {
7
+ endpoint: string;
8
+ response: Response;
9
+ logger: IntegrationLogger;
10
+ logErrorBody: boolean;
4
11
  }
5
- export declare class APIError extends Error {
6
- /**
7
- * Code associated with the error.
8
- */
9
- readonly code: number;
10
- constructor(config: APIErrorParams);
12
+ export declare class RetryableIntegrationProviderApiError extends IntegrationProviderAPIError {
13
+ retryable: boolean;
11
14
  }
15
+ export declare class RateLimitError extends RetryableIntegrationProviderApiError {
16
+ constructor(options: RateLimitErrorParams);
17
+ retryAfter: number;
18
+ }
19
+ export declare class ResponseBodyError extends Error {
20
+ bodyError: string;
21
+ constructor(bodyError: string);
22
+ }
23
+ export declare function retryableRequestError({ endpoint, response, logger, logErrorBody, }: RequestErrorParams): Promise<RetryableIntegrationProviderApiError>;
24
+ export declare function fatalRequestError({ endpoint, response, logger, logErrorBody, }: RequestErrorParams): Promise<IntegrationProviderAPIError>;
25
+ /**
26
+ * Function for determining if a request is retryable
27
+ * based on the returned status.
28
+ */
29
+ export declare function isRetryableRequest(status: number): boolean;
30
+ export {};