Last active
November 3, 2024 01:36
-
-
Save itsprofcjs/1dbfe4603fb4ff897899a1b9d7977544 to your computer and use it in GitHub Desktop.
Rxjs http fetch call with retry and backoff feature
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { fromFetch } from 'rxjs/fetch'; | |
import { catchError, delay, mergeMap, retryWhen, switchMap } from 'rxjs/operators'; | |
import { from, Observable, of, throwError } from 'rxjs'; | |
const retryWithBackOff = (delayMs: number = 0, maxRetry: number = 0, backOffMs: number = 0) => { | |
let currentRetries = maxRetry; | |
return (source: Observable<any>) => | |
source.pipe( | |
retryWhen((error: Observable<any>) => | |
error.pipe( | |
mergeMap((error) => { | |
if (currentRetries-- > 0) { | |
const currentBackOffTime = delayMs + (maxRetry - currentRetries) * backOffMs; | |
return of(error).pipe(delay(currentBackOffTime)); | |
} | |
return throwError(error); | |
}) | |
) | |
) | |
); | |
}; | |
/** | |
* | |
* @param error | |
* @param response | |
* Save response to check later for validating in apiErrorHandler. | |
* This is to know that we actually received the response from server. | |
* Which may be of status 400 and even 500. | |
* Thus ensuring that api is not failed by browser itself | |
*/ | |
const mapErrorResponse = (error: any, response: Response) => { | |
let errorResponse = { data: error, response }; | |
if (response.status >= 500) { | |
errorResponse.data = { data: null, message: 'We are facing some technical dificulty' }; | |
} | |
return throwError(errorResponse); | |
}; | |
/** | |
* | |
* To handle all type of error in api call. | |
* If mapErrorResponse() was called then we should receive this | |
* `{data : any , response : Response }` structure | |
* If response is undefined, then it means api call was not succesfully executed. | |
* It may be due to "NO_INTERNET", "REQUEST_TIMEOUT", error in creating request etc | |
* | |
*/ | |
const apiErrorhandler = ({ data, response }: any = {}) => { | |
let sanitizedError; | |
if (!response) { | |
/** | |
* | |
* Its now confirmed that we have some browser error | |
*/ | |
sanitizedError = { data: '', message: 'Error Reaching our server!' }; | |
} else { | |
sanitizedError = data; | |
} | |
return throwError(sanitizedError); | |
}; | |
const getResponseType = (response: Response) => { | |
const contentType = (response.headers.get('content-type') + '').toLocaleLowerCase(); | |
let bodyType: 'json' | 'text' | 'raw'; | |
if (contentType.indexOf('application/json') !== -1) { | |
bodyType = 'json'; | |
} else if (contentType.indexOf('text/plain') !== -1) { | |
bodyType = 'text'; | |
} else { | |
bodyType = 'raw'; | |
} | |
return bodyType; | |
}; | |
const getResponseBody = (response: Response) => { | |
const bodyType = getResponseType(response); | |
if (bodyType === 'json') { | |
return response.json(); | |
} else if (bodyType === 'text') { | |
return response.text(); | |
} else { | |
return response.blob(); | |
} | |
}; | |
/** | |
* | |
* Fetch api treats all the response from server as success. | |
* It means status code in range of 400,500 are treated as success. | |
* This is to treat such response as failure | |
* @param response | |
*/ | |
const handleApiResponse = (response: Response, rawResponse: boolean = false) => { | |
if (response.ok && response.status >= 200 && response.status < 300) { | |
return rawResponse ? of(response) : getResponseBody(response); | |
} else { | |
if (rawResponse) { | |
return mapErrorResponse(response, response); | |
} else { | |
return from(getResponseBody(response)).pipe( | |
switchMap((body) => { | |
return mapErrorResponse(body, response); | |
}) | |
); | |
} | |
} | |
}; | |
interface RequestConfig extends RequestInit { | |
delayMs?: number; | |
maxRetry?: number; | |
backOffMs?: number; | |
rawResponse?: boolean; | |
} | |
const getApiAgent = (url: string | Request, init: RequestConfig) => { | |
const { delayMs, maxRetry, backOffMs, rawResponse, ...requestInit } = init; | |
return fromFetch(url, requestInit).pipe( | |
switchMap((response) => { | |
return handleApiResponse(response, rawResponse); | |
}), | |
retryWithBackOff(delayMs, maxRetry, backOffMs), | |
catchError(apiErrorhandler) | |
); | |
}; | |
export const apiService = { | |
get(url: string | Request, init?: RequestConfig) { | |
const config = init ? { ...init, method: 'GET' } : { method: 'GET' }; | |
return getApiAgent(url, config); | |
}, | |
post(url: string | Request, init?: RequestConfig) { | |
const config = init ? { ...init, method: 'POST' } : { method: 'POST' }; | |
return getApiAgent(url, config); | |
}, | |
put(url: string | Request, init?: RequestConfig) { | |
const config = init ? { ...init, method: 'PUT' } : { method: 'PUT' }; | |
return getApiAgent(url, config); | |
}, | |
delete(url: string | Request, init?: RequestConfig) { | |
const config = init ? { ...init, method: 'DELETE' } : { method: 'DELETE' }; | |
return getApiAgent(url, config); | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment