diff --git a/src/common/exceptions/bad-request.exception.ts b/src/common/exceptions/bad-request.exception.ts index b86b23953..3adf24488 100644 --- a/src/common/exceptions/bad-request.exception.ts +++ b/src/common/exceptions/bad-request.exception.ts @@ -1,6 +1,8 @@ -export class BadRequestException extends Error { - readonly status: number = 400; - readonly name: string = 'BadRequestException'; +import { RequestException } from '../interfaces/request-exception.interface'; + +export class BadRequestException extends Error implements RequestException { + readonly status = 400; + readonly name = 'BadRequestException'; readonly message: string = 'Bad request'; readonly code?: string; readonly errors?: unknown[]; diff --git a/src/common/exceptions/generic-server.exception.ts b/src/common/exceptions/generic-server.exception.ts index fa4349525..a9241eed3 100644 --- a/src/common/exceptions/generic-server.exception.ts +++ b/src/common/exceptions/generic-server.exception.ts @@ -1,4 +1,6 @@ -export class GenericServerException extends Error { +import { RequestException } from '../interfaces/request-exception.interface'; + +export class GenericServerException extends Error implements RequestException { readonly name: string = 'GenericServerException'; readonly message: string = 'The request could not be completed.'; diff --git a/src/common/exceptions/index.ts b/src/common/exceptions/index.ts index 5a2445656..845f0d6ed 100644 --- a/src/common/exceptions/index.ts +++ b/src/common/exceptions/index.ts @@ -3,6 +3,7 @@ export * from './bad-request.exception'; export * from './no-api-key-provided.exception'; export * from './not-found.exception'; export * from './oauth.exception'; +export * from './rate-limit-exceeded.exception'; export * from './signature-verification.exception'; export * from './unauthorized.exception'; export * from './unprocessable-entity.exception'; diff --git a/src/common/exceptions/no-api-key-provided.exception.ts b/src/common/exceptions/no-api-key-provided.exception.ts index fd2223f70..595cbb8f0 100644 --- a/src/common/exceptions/no-api-key-provided.exception.ts +++ b/src/common/exceptions/no-api-key-provided.exception.ts @@ -1,7 +1,7 @@ export class NoApiKeyProvidedException extends Error { - readonly status: number = 500; - readonly name: string = 'NoApiKeyProvidedException'; - readonly message: string = + readonly status = 500; + readonly name = 'NoApiKeyProvidedException'; + readonly message = `Missing API key. Pass it to the constructor (new WorkOS("sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU")) ` + `or define it in the WORKOS_API_KEY environment variable.`; } diff --git a/src/common/exceptions/not-found.exception.ts b/src/common/exceptions/not-found.exception.ts index 3088f54fa..39a3ba7d8 100644 --- a/src/common/exceptions/not-found.exception.ts +++ b/src/common/exceptions/not-found.exception.ts @@ -1,6 +1,8 @@ -export class NotFoundException extends Error { - readonly status: number = 404; - readonly name: string = 'NotFoundException'; +import { RequestException } from '../interfaces/request-exception.interface'; + +export class NotFoundException extends Error implements RequestException { + readonly status = 404; + readonly name = 'NotFoundException'; readonly message: string; readonly code?: string; readonly requestID: string; diff --git a/src/common/exceptions/oauth.exception.ts b/src/common/exceptions/oauth.exception.ts index 8ceb045df..14f5cf6f6 100644 --- a/src/common/exceptions/oauth.exception.ts +++ b/src/common/exceptions/oauth.exception.ts @@ -1,5 +1,7 @@ -export class OauthException extends Error { - readonly name: string = 'OauthException'; +import { RequestException } from '../interfaces/request-exception.interface'; + +export class OauthException extends Error implements RequestException { + readonly name = 'OauthException'; constructor( readonly status: number, diff --git a/src/common/exceptions/rate-limit-exceeded.exception.ts b/src/common/exceptions/rate-limit-exceeded.exception.ts new file mode 100644 index 000000000..8b781345d --- /dev/null +++ b/src/common/exceptions/rate-limit-exceeded.exception.ts @@ -0,0 +1,20 @@ +import { GenericServerException } from './generic-server.exception'; + +// Inheriting from `GenericServerException` in order to maintain backwards +// compatibility with what 429 errors would have previously been thrown as. +// +// TODO: Consider making it the base class for all request errors. +export class RateLimitExceededException extends GenericServerException { + readonly name = 'RateLimitExceededException'; + + constructor( + message: string, + requestID: string, + /** + * The number of seconds to wait before retrying the request. + */ + readonly retryAfter: number | null, + ) { + super(429, message, {}, requestID); + } +} diff --git a/src/common/exceptions/signature-verification.exception.ts b/src/common/exceptions/signature-verification.exception.ts index d699bc830..e8abf667c 100644 --- a/src/common/exceptions/signature-verification.exception.ts +++ b/src/common/exceptions/signature-verification.exception.ts @@ -1,5 +1,5 @@ export class SignatureVerificationException extends Error { - readonly name: string = 'SignatureVerificationException'; + readonly name = 'SignatureVerificationException'; constructor(message: string) { super(message || 'Signature verification failed.'); diff --git a/src/common/exceptions/unauthorized.exception.ts b/src/common/exceptions/unauthorized.exception.ts index a6e0d1c03..93c9f3a9d 100644 --- a/src/common/exceptions/unauthorized.exception.ts +++ b/src/common/exceptions/unauthorized.exception.ts @@ -1,6 +1,8 @@ -export class UnauthorizedException extends Error { - readonly status: number = 401; - readonly name: string = 'UnauthorizedException'; +import { RequestException } from '../interfaces/request-exception.interface'; + +export class UnauthorizedException extends Error implements RequestException { + readonly status = 401; + readonly name = 'UnauthorizedException'; readonly message: string; constructor(readonly requestID: string) { diff --git a/src/common/exceptions/unprocessable-entity.exception.ts b/src/common/exceptions/unprocessable-entity.exception.ts index 95126d161..da5770297 100644 --- a/src/common/exceptions/unprocessable-entity.exception.ts +++ b/src/common/exceptions/unprocessable-entity.exception.ts @@ -1,10 +1,14 @@ import pluralize from 'pluralize'; import { UnprocessableEntityError } from '../interfaces'; - -export class UnprocessableEntityException extends Error { - readonly status: number = 422; - readonly name: string = 'UnprocessableEntityException'; +import { RequestException } from '../interfaces/request-exception.interface'; + +export class UnprocessableEntityException + extends Error + implements RequestException +{ + readonly status = 422; + readonly name = 'UnprocessableEntityException'; readonly message: string = 'Unprocessable entity'; readonly code?: string; readonly requestID: string; diff --git a/src/common/interfaces/request-exception.interface.ts b/src/common/interfaces/request-exception.interface.ts new file mode 100644 index 000000000..1731bd509 --- /dev/null +++ b/src/common/interfaces/request-exception.interface.ts @@ -0,0 +1,5 @@ +export interface RequestException { + readonly status: number; + readonly message: string; + readonly requestID: string; +} diff --git a/src/workos.spec.ts b/src/workos.spec.ts index adf4a3aac..a5421dd80 100644 --- a/src/workos.spec.ts +++ b/src/workos.spec.ts @@ -8,6 +8,7 @@ import { OauthException, } from './common/exceptions'; import { WorkOS } from './workos'; +import { RateLimitExceededException } from './common/exceptions/rate-limit-exceeded.exception'; describe('WorkOS', () => { beforeEach(() => fetch.resetMocks()); @@ -198,6 +199,7 @@ describe('WorkOS', () => { ); }); }); + describe('when the api responds with a 400 and an error/error_description', () => { it('throws an OauthException', async () => { fetchOnce( @@ -222,6 +224,30 @@ describe('WorkOS', () => { }); }); + describe('when the api responses with a 429', () => { + it('throws a RateLimitExceededException', async () => { + fetchOnce( + { + message: 'Too many requests', + }, + { + status: 429, + headers: { 'X-Request-ID': 'a-request-id', 'Retry-After': '10' }, + }, + ); + + const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); + + await expect(workos.get('/path')).rejects.toStrictEqual( + new RateLimitExceededException( + 'Too many requests', + 'a-request-id', + 10, + ), + ); + }); + }); + describe('when the entity is null', () => { it('sends a null body', async () => { fetchOnce(); diff --git a/src/workos.ts b/src/workos.ts index a61c53cce..2a1a9a55b 100644 --- a/src/workos.ts +++ b/src/workos.ts @@ -5,6 +5,7 @@ import { UnauthorizedException, UnprocessableEntityException, OauthException, + RateLimitExceededException, } from './common/exceptions'; import { GetOptions, @@ -214,6 +215,15 @@ export class WorkOS { requestID, }); } + case 429: { + const retryAfter = headers.get('Retry-After'); + + throw new RateLimitExceededException( + data.message, + requestID, + retryAfter ? Number(retryAfter) : null, + ); + } default: { if (error || errorDescription) { throw new OauthException(