fix(account): add retry and rate-limit backoff handling for API requests

This commit is contained in:
2026-03-28 10:42:47 +00:00
parent d27d5e3537
commit e73bc44277
4 changed files with 46 additions and 12 deletions

View File

@@ -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

View File

@@ -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.'
}

View File

@@ -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<ReturnType<typeof builder.get>>;
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<Uint8Array> {
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 */

View File

@@ -20,6 +20,9 @@ export async function autoPaginate<T>(
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;