From e73bc44277a17d1e0a9bba896a64bd258f3bb369 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 28 Mar 2026 10:42:47 +0000 Subject: [PATCH] fix(account): add retry and rate-limit backoff handling for API requests --- changelog.md | 7 +++++ ts/00_commitinfo_data.ts | 2 +- ts/bookstack.classes.account.ts | 46 +++++++++++++++++++++++++-------- ts/bookstack.helpers.ts | 3 +++ 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/changelog.md b/changelog.md index b4ca41b..fca767f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-28 - 1.1.1 - fix(account) +add retry and rate-limit backoff handling for API requests + +- Enable retries and 429 backoff handling for JSON and text API requests. +- Add exponential backoff retries for binary downloads when rate limited. +- Introduce a small delay between paginated requests to reduce API throttling. + ## 2026-03-28 - 1.1.0 - feat(build) modernize the build configuration and add project documentation diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 3a7068d..80ad835 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@apiclient.xyz/bookstack', - version: '1.1.0', + version: '1.1.1', description: 'A TypeScript API client for BookStack, providing easy access to books, chapters, pages, shelves, and more.' } diff --git a/ts/bookstack.classes.account.ts b/ts/bookstack.classes.account.ts index 8b3124a..828f797 100644 --- a/ts/bookstack.classes.account.ts +++ b/ts/bookstack.classes.account.ts @@ -51,7 +51,9 @@ export class BookStackAccount { let builder = plugins.smartrequest.SmartRequest.create() .url(url) .header('Authorization', `Token ${this.tokenId}:${this.tokenSecret}`) - .header('Content-Type', 'application/json'); + .header('Content-Type', 'application/json') + .retry(3) + .handle429Backoff({ maxRetries: 5, maxWaitTime: 30000, fallbackDelay: 2000 }); if (data) { builder = builder.json(data); @@ -73,6 +75,10 @@ export class BookStackAccount { break; } + if (response.status === 429) { + throw new Error(`Rate limited: ${method} ${path} — retries exhausted`); + } + if (!response.ok) { const errorText = await response.text(); throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`); @@ -95,7 +101,9 @@ export class BookStackAccount { let builder = plugins.smartrequest.SmartRequest.create() .url(url) .header('Authorization', `Token ${this.tokenId}:${this.tokenSecret}`) - .header('Accept', 'text/plain'); + .header('Accept', 'text/plain') + .retry(3) + .handle429Backoff({ maxRetries: 5, maxWaitTime: 30000, fallbackDelay: 2000 }); let response: Awaited>; switch (method) { @@ -113,6 +121,10 @@ export class BookStackAccount { break; } + if (response.status === 429) { + throw new Error(`Rate limited: ${method} ${path} — retries exhausted`); + } + if (!response.ok) { const errorText = await response.text(); throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`); @@ -124,16 +136,28 @@ export class BookStackAccount { /** @internal */ async requestBinary(path: string): Promise { const url = `${this.baseUrl}/api${path}`; - const response = await fetch(url, { - headers: { - Authorization: `Token ${this.tokenId}:${this.tokenSecret}`, - }, - }); - if (!response.ok) { - throw new Error(`GET ${path}: ${response.status} ${response.statusText}`); + const maxRetries = 3; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + const response = await fetch(url, { + headers: { + Authorization: `Token ${this.tokenId}:${this.tokenSecret}`, + }, + }); + if (response.status === 429) { + if (attempt === maxRetries) { + throw new Error(`Rate limited: GET ${path} — retries exhausted`); + } + const delay = 2000 * Math.pow(2, attempt); + await new Promise((r) => setTimeout(r, delay)); + continue; + } + if (!response.ok) { + throw new Error(`GET ${path}: ${response.status} ${response.statusText}`); + } + const buf = await response.arrayBuffer(); + return new Uint8Array(buf); } - const buf = await response.arrayBuffer(); - return new Uint8Array(buf); + throw new Error(`GET ${path}: max retries exceeded`); } /** @internal — build a URL with list query params */ diff --git a/ts/bookstack.helpers.ts b/ts/bookstack.helpers.ts index 2b7760d..cd51865 100644 --- a/ts/bookstack.helpers.ts +++ b/ts/bookstack.helpers.ts @@ -20,6 +20,9 @@ export async function autoPaginate( const all: T[] = []; let offset = 0; while (true) { + if (offset > 0) { + await new Promise((r) => setTimeout(r, 100)); + } const result = await fetchPage(offset, count); all.push(...result.data); offset += count;