From 2a8db2af5d5a7f42641dcb79e14b175c2d3bb0f2 Mon Sep 17 00:00:00 2001 From: Taombawkry Date: Thu, 8 Aug 2024 14:32:02 +0200 Subject: [PATCH 1/3] chore; type addition --- nodejs/tests/api.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index d56a846..2acaadc 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -43,7 +43,7 @@ test('should throw axios error object if set wrapResponseErrors to false', async }) server.use( - rest.get('https://api.hackmd.io/v1/me', (req, res, ctx) => { + rest.get('https://api.hackmd.io/v1/me', (req: any, res: (arg0: any) => any, ctx: { status: (arg0: number) => any }) => { return res(ctx.status(429)) }), ) From 2dd5edbd6ff4344c43e3511ff193742707a29a0e Mon Sep 17 00:00:00 2001 From: Taombawkry Date: Thu, 8 Aug 2024 14:46:14 +0200 Subject: [PATCH 2/3] Add retry logic with exponential backoff to API client --- nodejs/src/index.ts | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index eef0246..a45d1e7 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -28,7 +28,8 @@ export class API { baseURL: hackmdAPIEndpointURL, headers:{ "Content-Type": "application/json", - } + }, + timeout: 30000, // Increased timeout for low bandwidth }) this.axios.interceptors.request.use( @@ -71,13 +72,48 @@ export class API { `Received an error response (${err.response.status} ${err.response.statusText}) from HackMD`, err.response.status, err.response.statusText, - ) + ); } } - ) + ); } + this.createRetryInterceptor(this.axios, 3); // Add retry interceptor with maxRetries = 3 + } + + // Utility functions for exponential backoff and retry logic + private exponentialBackoff(retries: number): number { + return Math.pow(2, retries) * 100; // Exponential backoff with base delay of 100ms + } + + private isRetryableError(error: AxiosError): boolean { + // Retry on network errors, 5xx errors, and rate limiting (429) + return ( + !error.response || + (error.response.status >= 500 && error.response.status < 600) || + error.response.status === 429 + ); } + // Create retry interceptor function + private createRetryInterceptor(axiosInstance: AxiosInstance, maxRetries: number): void { + let retryCount = 0; + + axiosInstance.interceptors.response.use( + response => response, + async error => { + if (retryCount < maxRetries && this.isRetryableError(error)) { + retryCount++; + const delay = this.exponentialBackoff(retryCount); + console.warn(`Retrying request... attempt #${retryCount} after delay of ${delay}ms`); + await new Promise(resolve => setTimeout(resolve, delay)); + return axiosInstance(error.config); + } + + retryCount = 0; // Reset retry count after a successful request + return Promise.reject(error); + } + ); + } async getMe (options = defaultOption as Opt): Promise> { return this.unwrapData(this.axios.get("me"), options.unwrapData) as unknown as OptionReturnType } From 93a00603ac79062f4c68867af7640a7b5b2feb4a Mon Sep 17 00:00:00 2001 From: Thomas Gondwe Date: Fri, 6 Sep 2024 09:35:07 +0000 Subject: [PATCH 3/3] feat: expand client options type and add credit check with retry logic --- nodejs/src/index.ts | 51 +++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index a45d1e7..21ea9cd 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -13,13 +13,29 @@ const defaultOption: RequestOptions = { type OptionReturnType = Opt extends { unwrapData: false } ? AxiosResponse : Opt extends { unwrapData: true } ? T : T export type APIClientOptions = { - wrapResponseErrors: boolean + wrapResponseErrors: boolean; + timeout?: number; + retryConfig?: { + maxRetries: number; + baseDelay: number; + }; } export class API { private axios: AxiosInstance - constructor (readonly accessToken: string, public hackmdAPIEndpointURL: string = "https://api.hackmd.io/v1", public options: APIClientOptions = { wrapResponseErrors: true }) { + constructor ( + readonly accessToken: string, + public hackmdAPIEndpointURL: string = "https://api.hackmd.io/v1", + public options: APIClientOptions = { + wrapResponseErrors: true, + timeout: 30000, + retryConfig: { + maxRetries: 3, + baseDelay: 100, + }, + } + ) { if (!accessToken) { throw new HackMDErrors.MissingRequiredArgument('Missing access token when creating HackMD client') } @@ -29,7 +45,7 @@ export class API { headers:{ "Content-Type": "application/json", }, - timeout: 30000, // Increased timeout for low bandwidth + timeout: options.timeout }) this.axios.interceptors.request.use( @@ -77,16 +93,16 @@ export class API { } ); } - this.createRetryInterceptor(this.axios, 3); // Add retry interceptor with maxRetries = 3 + if (options.retryConfig) { + this.createRetryInterceptor(this.axios, options.retryConfig.maxRetries, options.retryConfig.baseDelay); + } } - // Utility functions for exponential backoff and retry logic - private exponentialBackoff(retries: number): number { - return Math.pow(2, retries) * 100; // Exponential backoff with base delay of 100ms + private exponentialBackoff(retries: number, baseDelay: number): number { + return Math.pow(2, retries) * baseDelay; } private isRetryableError(error: AxiosError): boolean { - // Retry on network errors, 5xx errors, and rate limiting (429) return ( !error.response || (error.response.status >= 500 && error.response.status < 600) || @@ -94,22 +110,25 @@ export class API { ); } - // Create retry interceptor function - private createRetryInterceptor(axiosInstance: AxiosInstance, maxRetries: number): void { + private createRetryInterceptor(axiosInstance: AxiosInstance, maxRetries: number, baseDelay: number): void { let retryCount = 0; axiosInstance.interceptors.response.use( response => response, async error => { if (retryCount < maxRetries && this.isRetryableError(error)) { - retryCount++; - const delay = this.exponentialBackoff(retryCount); - console.warn(`Retrying request... attempt #${retryCount} after delay of ${delay}ms`); - await new Promise(resolve => setTimeout(resolve, delay)); - return axiosInstance(error.config); + const remainingCredits = parseInt(error.response?.headers['x-ratelimit-userremaining'], 10); + + if (isNaN(remainingCredits) || remainingCredits > 0) { + retryCount++; + const delay = this.exponentialBackoff(retryCount, baseDelay); + console.warn(`Retrying request... attempt #${retryCount} after delay of ${delay}ms`); + await new Promise(resolve => setTimeout(resolve, delay)); + return axiosInstance(error.config); + } } - retryCount = 0; // Reset retry count after a successful request + retryCount = 0; // Reset retry count after a successful request or when not retrying return Promise.reject(error); } );