Skip to content

Commit 737366b

Browse files
authored
Merge pull request #30 from Taombawkry/develop
Add retry logic with exponential backoff to API client
2 parents 0c6f090 + 93a0060 commit 737366b

File tree

2 files changed

+61
-6
lines changed

2 files changed

+61
-6
lines changed

nodejs/src/index.ts

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,29 @@ const defaultOption: RequestOptions = {
1313
type OptionReturnType<Opt, T> = Opt extends { unwrapData: false } ? AxiosResponse<T> : Opt extends { unwrapData: true } ? T : T
1414

1515
export type APIClientOptions = {
16-
wrapResponseErrors: boolean
16+
wrapResponseErrors: boolean;
17+
timeout?: number;
18+
retryConfig?: {
19+
maxRetries: number;
20+
baseDelay: number;
21+
};
1722
}
1823

1924
export class API {
2025
private axios: AxiosInstance
2126

22-
constructor (readonly accessToken: string, public hackmdAPIEndpointURL: string = "https://api.hackmd.io/v1", public options: APIClientOptions = { wrapResponseErrors: true }) {
27+
constructor (
28+
readonly accessToken: string,
29+
public hackmdAPIEndpointURL: string = "https://api.hackmd.io/v1",
30+
public options: APIClientOptions = {
31+
wrapResponseErrors: true,
32+
timeout: 30000,
33+
retryConfig: {
34+
maxRetries: 3,
35+
baseDelay: 100,
36+
},
37+
}
38+
) {
2339
if (!accessToken) {
2440
throw new HackMDErrors.MissingRequiredArgument('Missing access token when creating HackMD client')
2541
}
@@ -28,7 +44,8 @@ export class API {
2844
baseURL: hackmdAPIEndpointURL,
2945
headers:{
3046
"Content-Type": "application/json",
31-
}
47+
},
48+
timeout: options.timeout
3249
})
3350

3451
this.axios.interceptors.request.use(
@@ -71,13 +88,51 @@ export class API {
7188
`Received an error response (${err.response.status} ${err.response.statusText}) from HackMD`,
7289
err.response.status,
7390
err.response.statusText,
74-
)
91+
);
7592
}
7693
}
77-
)
94+
);
95+
}
96+
if (options.retryConfig) {
97+
this.createRetryInterceptor(this.axios, options.retryConfig.maxRetries, options.retryConfig.baseDelay);
7898
}
7999
}
80100

101+
private exponentialBackoff(retries: number, baseDelay: number): number {
102+
return Math.pow(2, retries) * baseDelay;
103+
}
104+
105+
private isRetryableError(error: AxiosError): boolean {
106+
return (
107+
!error.response ||
108+
(error.response.status >= 500 && error.response.status < 600) ||
109+
error.response.status === 429
110+
);
111+
}
112+
113+
private createRetryInterceptor(axiosInstance: AxiosInstance, maxRetries: number, baseDelay: number): void {
114+
let retryCount = 0;
115+
116+
axiosInstance.interceptors.response.use(
117+
response => response,
118+
async error => {
119+
if (retryCount < maxRetries && this.isRetryableError(error)) {
120+
const remainingCredits = parseInt(error.response?.headers['x-ratelimit-userremaining'], 10);
121+
122+
if (isNaN(remainingCredits) || remainingCredits > 0) {
123+
retryCount++;
124+
const delay = this.exponentialBackoff(retryCount, baseDelay);
125+
console.warn(`Retrying request... attempt #${retryCount} after delay of ${delay}ms`);
126+
await new Promise(resolve => setTimeout(resolve, delay));
127+
return axiosInstance(error.config);
128+
}
129+
}
130+
131+
retryCount = 0; // Reset retry count after a successful request or when not retrying
132+
return Promise.reject(error);
133+
}
134+
);
135+
}
81136
async getMe<Opt extends RequestOptions> (options = defaultOption as Opt): Promise<OptionReturnType<Opt, GetMe>> {
82137
return this.unwrapData(this.axios.get<GetMe>("me"), options.unwrapData) as unknown as OptionReturnType<Opt, GetMe>
83138
}

nodejs/tests/api.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ test('should throw axios error object if set wrapResponseErrors to false', async
4343
})
4444

4545
server.use(
46-
rest.get('https://api.hackmd.io/v1/me', (req, res, ctx) => {
46+
rest.get('https://api.hackmd.io/v1/me', (req: any, res: (arg0: any) => any, ctx: { status: (arg0: number) => any }) => {
4747
return res(ctx.status(429))
4848
}),
4949
)

0 commit comments

Comments
 (0)