@jupiterone/integration-sdk-http-client
Advanced tools
Comparing version 12.2.4 to 12.2.5
@@ -162,2 +162,3 @@ import { Response } from 'node-fetch'; | ||
}, dataPath: string | string[] | undefined, nextPageCallback: (data: NextPageData) => IterateCallbackResult | undefined): AsyncGenerator<T, void, unknown>; | ||
protected parseResponseInPaginate(response: Response): Promise<any>; | ||
/** | ||
@@ -164,0 +165,0 @@ * Get the token bucket for the given endpoint |
@@ -286,3 +286,3 @@ "use strict"; | ||
} | ||
const data = await response.json(); | ||
const data = await this.parseResponseInPaginate(response); | ||
let items = dataPath ? (0, get_1.default)(data, dataPath) : data; | ||
@@ -302,2 +302,5 @@ items = Array.isArray(items) ? items : []; | ||
} | ||
async parseResponseInPaginate(response) { | ||
return response.json(); | ||
} | ||
/** | ||
@@ -344,4 +347,3 @@ * Get the token bucket for the given endpoint | ||
const thresholdPercentage = this.rateLimitThrottling.threshold * 100; | ||
const resetHeaderName = this.rateLimitThrottling.rateLimitHeaders?.reset ?? | ||
'x-rate-limit-reset'; | ||
const resetHeaderName = this.rateLimitThrottling.rateLimitHeaders?.reset ?? 'ratelimit-reset'; | ||
this.logger.warn({ rateLimitLimit, rateLimitRemaining, timeToSleepInMs }, `Exceeded ${thresholdPercentage}% of rate limit. Sleeping until ${resetHeaderName}`); | ||
@@ -348,0 +350,0 @@ await (0, attempt_1.sleep)(timeToSleepInMs); |
@@ -21,3 +21,4 @@ import { IntegrationLogger, IntegrationProviderAPIError } from '@jupiterone/integration-sdk-core'; | ||
bodyError: string; | ||
constructor(bodyError: string); | ||
headers: Record<string, string>; | ||
constructor(bodyError: string, headers: Record<string, string>); | ||
} | ||
@@ -24,0 +25,0 @@ export declare function retryableRequestError({ endpoint, response, logger, logErrorBody, }: RequestErrorParams): Promise<RetryableIntegrationProviderApiError>; |
@@ -19,6 +19,8 @@ "use strict"; | ||
bodyError; | ||
constructor(bodyError) { | ||
headers; | ||
constructor(bodyError, headers) { | ||
super(`Error Response: ${bodyError}`); | ||
this.name = 'ResponseBodyError'; | ||
this.bodyError = bodyError; | ||
this.headers = headers; | ||
} | ||
@@ -41,7 +43,15 @@ } | ||
} | ||
function headersToRecord(headers) { | ||
const headersRecord = {}; | ||
for (const [key, value] of headers.entries()) { | ||
headersRecord[key] = value; | ||
} | ||
return headersRecord; | ||
} | ||
async function retryableRequestError({ endpoint, response, logger, logErrorBody, }) { | ||
const errorBody = await getErrorBody(response, logger, logErrorBody); | ||
const headers = headersToRecord(response.headers); | ||
if (response.status === 429) { | ||
return new RateLimitError({ | ||
cause: errorBody ? new ResponseBodyError(errorBody) : undefined, | ||
cause: errorBody ? new ResponseBodyError(errorBody, headers) : undefined, | ||
status: response.status, | ||
@@ -54,3 +64,3 @@ statusText: response.statusText, | ||
return new RetryableIntegrationProviderApiError({ | ||
cause: errorBody ? new ResponseBodyError(errorBody) : undefined, | ||
cause: errorBody ? new ResponseBodyError(errorBody, headers) : undefined, | ||
endpoint, | ||
@@ -64,4 +74,5 @@ status: response.status, | ||
const errorBody = await getErrorBody(response, logger, logErrorBody); | ||
const headers = headersToRecord(response.headers); | ||
const apiErrorOptions = { | ||
cause: errorBody ? new ResponseBodyError(errorBody) : undefined, | ||
cause: errorBody ? new ResponseBodyError(errorBody, headers) : undefined, | ||
endpoint, | ||
@@ -68,0 +79,0 @@ status: response.status, |
{ | ||
"name": "@jupiterone/integration-sdk-http-client", | ||
"version": "12.2.4", | ||
"version": "12.2.5", | ||
"description": "The HTTP client for use in JupiterOne integrations", | ||
@@ -27,3 +27,3 @@ "main": "dist/src/index.js", | ||
"@jupiterone/hierarchical-token-bucket": "^0.3.1", | ||
"@jupiterone/integration-sdk-core": "^12.2.4", | ||
"@jupiterone/integration-sdk-core": "^12.2.5", | ||
"@lifeomic/attempt": "^3.0.3", | ||
@@ -35,4 +35,4 @@ "form-data": "^4.0.0", | ||
"devDependencies": { | ||
"@jupiterone/integration-sdk-dev-tools": "^12.2.4", | ||
"@jupiterone/integration-sdk-private-test-utils": "^12.2.4", | ||
"@jupiterone/integration-sdk-dev-tools": "^12.2.5", | ||
"@jupiterone/integration-sdk-private-test-utils": "^12.2.5", | ||
"@types/node-fetch": "^2.6.11" | ||
@@ -44,3 +44,3 @@ }, | ||
"homepage": "https://github.com/JupiterOne/sdk#readme", | ||
"gitHead": "eba10b8647832f8965706f728589cb48c89923e4" | ||
"gitHead": "7838c08ef82cd2460b77ae2280d56d35f369aa2c" | ||
} |
204
README.md
# @jupiterone/integration-sdk-http-client | ||
This package contains the default HTTP Client used to interact with API | ||
providers. | ||
The Base API Client is a foundational class designed to simplify interactions | ||
with RESTful APIs. It abstracts common tasks such as making HTTP requests, | ||
handling retries, and managing rate limits. Built with flexibility in mind, it | ||
allows for easy extension to accommodate specific API endpoints of different | ||
services. | ||
### HTTP Client Features | ||
## Features | ||
The following checked items are implemented. Future features are listed but | ||
unchecked. | ||
- **Automatic Retries:** Implements an exponential backoff strategy for handling | ||
transient network and server errors. | ||
- **Pagination Support:** Includes utility methods for handling API pagination, | ||
making it easier to iterate over large data sets. | ||
- **Rate Limit Handling:** Monitors API usage against provided rate limit | ||
headers to prevent hitting API limits. | ||
- **Token Bucket Rate Limiting:** Optionally integrates a token bucket algorithm | ||
for fine-grained control over request rate limiting. | ||
- **Extensible Authentication:** Abstract method for setting authorization | ||
headers allows for flexible integration with different authentication | ||
mechanisms. | ||
- **Error Handling:** Provides a structured approach to handle and log API | ||
request errors, including support for custom error handling logic. | ||
- [x] v1 rate limit logic | ||
- [ ] pagination | ||
- [ ] error handling retries | ||
## Installation | ||
```bash | ||
yarn add node-fetch @jupiterone/integration-sdk-http-client | ||
``` | ||
npm install @jupiterone/integration-sdk-http-client | ||
# or | ||
## Usage | ||
yarn add @jupiterone/integration-sdk-http-client | ||
To use the Base API Client, extend it to create a custom client for your | ||
specific API. Implement the getAuthorizationHeaders method to provide the | ||
necessary authentication headers for your API requests. | ||
The `getAuthorizationHeaders` method doesn't need to be called manually, it's | ||
going to be called on the first request and saved and used for subsequent | ||
requests. | ||
Below are the details of each option available in the BaseAPIClient constructor: | ||
`baseUrl` (required) | ||
- **Type:** `string` | ||
- **Description:** The base URL for the API endpoints. All requests made through | ||
the client will use this URL as the base for constructing the full endpoint | ||
path. Exception: When a complete URL is sent to `request` or | ||
`retryableRequest` that will be used instead. | ||
`logger` (required) | ||
- **Type:** `IntegrationLogger` | ||
- **Description:** An instance of `IntegrationLogger` used by the client to log | ||
messages. This is typically provided by the integration context and should | ||
support various log levels (e.g., info, warn, error). | ||
`retryOptions` (optional) | ||
- **Type:** `Partial<RetryOptions>` | ||
- **Description:** Configuration options for controlling the behavior of request | ||
retries in case of failures. This is enabled by default. | ||
- `maxAttempts`: The maximum number of retry attempts. | ||
- `delay`: The initial delay between retries in milliseconds. | ||
- `timeout`: The maximum time to wait for a response before timing out. | ||
- `factor`: The factor by which the delay increases after each attempt (for | ||
exponential backoff). | ||
- `handleError`: A custom function invoked when an error occurs during a | ||
request attempt. This function allows for sophisticated error handling | ||
strategies, including conditional retries based on the type or severity of | ||
the error encountered. | ||
`logErrorBody` (optional) | ||
- **Type:** `boolean` | ||
- **Description:** Indicates whether the body of a response should be logged | ||
when an error occurs. Defaults to `false` to prevent logging sensitive | ||
information. | ||
`rateLimitThrottling` (optional) | ||
- **Type:** `RateLimitThrottlingOptions` | ||
- **Description:** Configuration options for handling API rate limits. A | ||
treshold needs to be specified to enable this feature. | ||
- `threshold`: A value between 0 and 1 indicating the percentage of rate limit | ||
utilization at which to start throttling requests. | ||
- `resetMode`: Specifies how to interpret the rate limit reset header. Options | ||
include `'remaining_epoch_s'` and `'datetime_epoch_s'`. Default is | ||
`remaining_epoch_s`. | ||
- `rateLimitHeaders`: Customizes the header names used to determine rate | ||
limits. Includes `limit`, `remaining`, and `reset` fields. Defaults are: | ||
`ratelimit-limit`, `ratelimit-remaining` and `ratelimit-reset`. | ||
`tokenBucket` (optional) | ||
- **Type:** `TokenBucketOptions` | ||
- **Description:** Configuration options for a token bucket rate limiting | ||
mechanism. | ||
- `maximumCapacity`: The maximum number of tokens the bucket can hold. | ||
- `refillRate`: The rate at which tokens are added to the bucket in a second. | ||
#### Example: Extending BaseAPIClient for a Custom API | ||
```typescript | ||
import { BaseAPIClient } from '@jupiterone/integration-sdk-http-client'; | ||
class CustomAPIClient extends BaseAPIClient { | ||
private config: IntegrationConfig; | ||
constructor(config: IntegrationConfig, logger: IntegrationLogger) { | ||
super({ | ||
baseUrl: `https://${config.accountId}.example.com/api`, | ||
logger, | ||
}); | ||
this.config = config; | ||
} | ||
protected async getAuthorizationHeaders(): Record<string, string> { | ||
return { | ||
Authorization: `Bearer ${this.config.apiToken}`, | ||
}; | ||
} | ||
// Add methods to interact with your specific API endpoints | ||
async fetchResource(): Promise<any> { | ||
const response = await this.retryableRequest('/your-api-endpoint'); | ||
return await response.json(); | ||
} | ||
} | ||
// Usage | ||
const client = new CustomAPIClient(config, logger); | ||
const data = await client.fetchResource(); | ||
``` | ||
There's some APIs that require to send a request first to get the Bearer token, | ||
that is also possible: | ||
```typescript | ||
protected async getAuthorizationHeaders(): Promise<Record<string, string>> { | ||
const tokenResponse = await this.request('/oauth/token', { | ||
method: 'POST', | ||
body: { | ||
username: this.username, | ||
password: this.password, | ||
}, | ||
authorize: false, // This is important to set when doing this request because otherwise `request()` is going to call this method going in a infinite loop. | ||
}); | ||
const data = await tokenResponse.json(); | ||
return { | ||
Authorization: `Bearer ${data.access_token}`, | ||
}; | ||
} | ||
``` | ||
--- | ||
### Pagination Support: | ||
To effectively use pagination with the BaseAPIClient class, you'll generally | ||
follow a pattern where you make an initial request to an endpoint that supports | ||
pagination and then continue to fetch subsequent pages of data based on the | ||
information provided in the response of each request. This process is often | ||
necessary for APIs that return large sets of data in chunks (pages) to reduce | ||
load on their servers and improve response times. | ||
The BaseAPIClient class provides an abstracted method, `paginate`, to facilitate | ||
the implementation of pagination in your API requests. Here's how you can use | ||
it: | ||
You'll need to send 3 parameters to the `paginate` method: | ||
- An initial request configuration (endpoint and options). | ||
- A path to the items in the API response. | ||
- A callback function that determines the parameters for fetching the next page, | ||
based on the current response. | ||
##### Example: Cursor-based pagination | ||
```typescript | ||
async iterateResources(iteratee: (resource: any) => Promise<void>): Promise<void> { | ||
const baseEndpoint = '/resources?limit=100'; | ||
const iterator = this.paginate( | ||
{ endpoint: baseEndpoint }, | ||
'data.items', // This is the path to access the array items, in this example we would iterate over the items in a response like { "data": { "items": [{ "id": 1, "name": "test-1" }, { "id": 2, "name": "test-2" }] } } | ||
(data) => { | ||
const { body } = data; | ||
// Assuming the nextPageData.body includes a `nextCursor` property | ||
const nextCursor = body.nextCursor; | ||
if (!nextCursor) { | ||
return; // No more pages | ||
} | ||
return { | ||
nextUrl: `${baseEndpoint}&cursor=${nextCursor}`, | ||
}; | ||
} | ||
); | ||
for await (const resource of iterator) { | ||
await iteratee(resource); | ||
} | ||
} | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
175847
806
204