Skip to content

Commit

Permalink
feat: add retry logic to 429 cases. (#119)
Browse files Browse the repository at this point in the history
* feat: add retry logic.

* chore: update jsonwebtoken lib

* fix: only retry in 429.

* tests: add basic test for 429 case.
  • Loading branch information
NicolasMontone authored Aug 5, 2024
1 parent f063748 commit edc69db
Show file tree
Hide file tree
Showing 9 changed files with 15,617 additions and 8,177 deletions.
3 changes: 3 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
PLUGGY_API_URL=https://test.com
9 changes: 9 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
testEnvironment: 'node',
transform: {
'^.+.tsx?$': ['ts-jest', {}],
},
setupFiles: ['<rootDir>/tests/setupTests.ts'],
testMatch: ['<rootDir>/tests/**/*.test.ts'],
}
23,665 changes: 15,503 additions & 8,162 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
],
"license": "MIT",
"scripts": {
"test": "jest",
"build": "tsc",
"prepare": "npm run build",
"prettier-fix": "prettier --write \"src/**/*.ts\"",
Expand All @@ -26,21 +27,26 @@
],
"dependencies": {
"got": "11.8.6",
"jsonwebtoken": "^8.5.1"
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"@semantic-release/commit-analyzer": "^9.0.2",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^9.2.5",
"@semantic-release/npm": "^11.0.2",
"@semantic-release/release-notes-generator": "^12.1.0",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^8.5.4",
"@types/node": "^16.11.7",
"@typescript-eslint/eslint-plugin": "^2.16.0",
"@typescript-eslint/parser": "^2.16.0",
"dotenv": "^16.4.5",
"eslint": "^6.8.0",
"jest": "^29.7.0",
"nock": "^13.5.4",
"prettier": "^1.19.1",
"semantic-release": "^22.0.12",
"ts-jest": "^29.2.4",
"ts-node": "^9.1.1",
"typescript": "^4.6.2"
},
Expand Down
49 changes: 36 additions & 13 deletions src/baseApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import got, { HTTPError, Method } from 'got'
import got, { Got, HTTPError, Method } from 'got'
import * as jwt from 'jsonwebtoken'
import { deserializeJSONWithDates } from './transforms'

Expand All @@ -8,6 +8,8 @@ const {
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require('../package.json')

const _60_SECONDS = 60 * 1000

type QueryParameters = { [key: string]: number | number[] | string | string[] | boolean }

export type ClientParams = {
Expand All @@ -24,6 +26,7 @@ export class BaseApi {
protected clientSecret: string
protected baseUrl?: string
protected defaultHeaders: Record<string, string>
protected serviceInstance: Got

constructor(params: ClientParams) {
const { clientId, clientSecret } = params
Expand All @@ -46,19 +49,41 @@ export class BaseApi {
}
}

private getServiceInstance(): Got {
if (!this.serviceInstance) {
this.serviceInstance = got.extend({
headers: this.defaultHeaders,
responseType: 'json',
parseJson: deserializeJSONWithDates,
retry: {
limit: 3,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
statusCodes: [429],
calculateDelay: ({ retryAfter }): number => {
return retryAfter ?? _60_SECONDS
},
},
})
}
return this.serviceInstance
}

protected async getApiKey(): Promise<string> {
if (this.apiKey && !this.isJwtExpired(this.apiKey)) {
return this.apiKey
}

const response = await got.post<{ apiKey: string }>(`${this.baseUrl}/auth`, {
json: {
clientId: this.clientId,
clientSecret: this.clientSecret,
nonExpiring: false,
},
responseType: 'json',
})
const response = await this.getServiceInstance().post<{ apiKey: string }>(
`${this.baseUrl}/auth`,
{
json: {
clientId: this.clientId,
clientSecret: this.clientSecret,
nonExpiring: false,
},
responseType: 'json',
}
)

this.apiKey = response.body.apiKey
return this.apiKey
Expand All @@ -69,7 +94,7 @@ export class BaseApi {
const url = `${this.baseUrl}/${endpoint}${this.mapToQueryString(params)}`

try {
const { statusCode, body } = await got<T>(url, {
const { statusCode, body } = await this.getServiceInstance()<T>(url, {
headers: {
...this.defaultHeaders,
'X-API-KEY': apiKey,
Expand Down Expand Up @@ -140,15 +165,13 @@ export class BaseApi {
}

try {
const { statusCode, body: responseBody } = await got<T>(url, {
const { statusCode, body: responseBody } = await this.getServiceInstance()<T>(url, {
method,
headers: {
...this.defaultHeaders,
'X-API-KEY': apiKey,
},
json: body,
responseType: 'json',
parseJson: deserializeJSONWithDates,
})

if (statusCode !== 200) {
Expand Down
48 changes: 48 additions & 0 deletions tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as nock from 'nock'
import { setupAuth } from './utils'
import { PluggyClient } from '../src/client'

describe('Auth', () => {
beforeEach(() => {
nock.cleanAll() // Clear previous mocks first
setupAuth()
})

it('creates a connect token', async () => {
const mockConnectTokenRequest = nock(process.env.PLUGGY_API_URL!)
.post('/connect_token')
.reply(200, { accessToken: '123' })

const client = new PluggyClient({
clientId: '123',
clientSecret: '456',
})

const token = await client.createConnectToken()

expect(token.accessToken).toEqual('123')

expect(mockConnectTokenRequest.isDone()).toBeTruthy()
})

it('with 429 status code, retries the request', async () => {
const mockConnectTokenRequest = nock(process.env.PLUGGY_API_URL!)
.post('/connect_token')
.reply(429, 'Rate limit exceeded', {
'Retry-After': '1',
})
.post('/connect_token')
.reply(200, { accessToken: '123' })

const client = new PluggyClient({
clientId: '123',
clientSecret: '456',
})

const token = await client.createConnectToken()

expect(token.accessToken).toEqual('123')

expect(mockConnectTokenRequest.isDone()).toBeTruthy()
})
})
3 changes: 3 additions & 0 deletions tests/setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as dotenv from 'dotenv'

dotenv.config({ path: '.env.test' })
7 changes: 7 additions & 0 deletions tests/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as nock from 'nock'

export function setupAuth(): void {
nock(process.env.PLUGGY_API_URL!)
.post('/auth')
.reply(200, { apiKey: '123' })
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "example"]
"exclude": ["node_modules", "example", "tests"]
}

0 comments on commit edc69db

Please sign in to comment.