diff --git a/test/test.statements.ts b/test/test.statements.ts index aeef430..c9f2aca 100644 --- a/test/test.statements.ts +++ b/test/test.statements.ts @@ -162,8 +162,6 @@ tap.test('should create and download PDF statement', async () => { }); tap.test('should create CSV statement with custom date range', async () => { - // Wait to avoid rate limiting - await new Promise(resolve => setTimeout(resolve, 3500)); console.log('Creating CSV statement export...'); // Use last month's date range to ensure it's in the past @@ -200,8 +198,6 @@ tap.test('should create CSV statement with custom date range', async () => { }); tap.test('should create MT940 statement', async () => { - // Wait to avoid rate limiting - await new Promise(resolve => setTimeout(resolve, 3500)); console.log('Creating MT940 statement export...'); const exportBuilder = primaryAccount.getAccountStatement({ diff --git a/ts/bunq.classes.httpclient.ts b/ts/bunq.classes.httpclient.ts index d0e811f..c78aae7 100644 --- a/ts/bunq.classes.httpclient.ts +++ b/ts/bunq.classes.httpclient.ts @@ -24,9 +24,46 @@ export class BunqHttpClient { } /** - * Make an API request to bunq + * Make an API request to bunq with automatic retry on rate limit */ public async request(options: IBunqRequestOptions): Promise { + const maxRetries = 3; + let lastError: Error; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await this.makeRequest(options); + } catch (error) { + lastError = error as Error; + + // Check if it's a rate limit error + if (error instanceof BunqApiError) { + const isRateLimitError = error.errors.some(e => + e.error_description.includes('Too many requests') || + e.error_description.includes('rate limit') + ); + + if (isRateLimitError && attempt < maxRetries) { + // Exponential backoff: 1s, 2s, 4s + const backoffMs = Math.pow(2, attempt) * 1000; + console.log(`Rate limit hit, backing off for ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`); + await new Promise(resolve => setTimeout(resolve, backoffMs)); + continue; + } + } + + // For non-rate-limit errors or if we've exhausted retries, throw immediately + throw error; + } + } + + throw lastError!; + } + + /** + * Internal method to make the actual request + */ + private async makeRequest(options: IBunqRequestOptions): Promise { let url = `${this.context.baseUrl}${options.endpoint}`; // Prepare headers