Skip to content

Instantly share code, notes, and snippets.

@marianopaulin02
Last active May 23, 2021 20:11
Show Gist options
  • Save marianopaulin02/5d988a04aa1a54023bd1bc68bd95dd7c to your computer and use it in GitHub Desktop.
Save marianopaulin02/5d988a04aa1a54023bd1bc68bd95dd7c to your computer and use it in GitHub Desktop.
Javascript fetch with retry and abort requests

Fetch API with superpowers 💪

With this script you can

  • Chain methods to build your request
  • Retry failed request
  • Abort requests

Instance the object

const req = fetchAPI()

Define endpoint

.endpoint()

Define path

.uri()

Headers

.headers()

Query params

Accepts array of strings ['a=1','b=2'] or object {a:1, b:2}

.params()

Retry intents

.maxIntents(int)

Methods

Finally call the method to make the request

.get(), .post(body), .patch(body), .delete() and you can add your own

Full example 🙌

import fetchAPI from 'fetchAPI'

const options = { maxIntents: 2 }
const req = fetchAPI( options )
    .endpoint('https://jsonplaceholder.typicode.com')
    .uri('todos/1')
    .headers({Authorization: 'Bearer Token', 'content-type':'application/json'})
    .params({a:1, b:2})
    .get()

req.then(res => console.log('ok', res))
req.catch(err => console.log('error', err))

// abort?
// req.abort()

Licence: MIT

const RETRY_INTERVALS = [0, 4, 8, 16, 32, 64] // seconds to wait for retry the request
const DEFAULT_TIMEOUT_MS = 15000
const fetchAPI = ({
// DOCS https://gist.github.com/marianopaulin02/5d988a04aa1a54023bd1bc68bd95dd7c
endpoint = '/', // Your URL
uri = '', // Your URI, or Can be included in endpoint
maxIntents = 1, // How many retry intents
timeout = DEFAULT_TIMEOUT_MS, // Max waiting time for a request before abort
rawResponse = false, // Return raw fetch response, else process as json
rawBody = false, // by dafault JSON.stringify is applied to body
useAuthorization = null, // async function to get value for Authorization header
...extraFetchOptions // Additional options for fetch()
} = {}) => {
const config = {
method: 'GET',
uri: '',
endpoint,
query: [],
headers: {},
maxIntents: maxIntents > 0 ? maxIntents : 1,
timeout,
useAuthorization,
}
let intents = 0
const doRequest = () => {
let abortRequest = null
let intentTimeout = null
let intentIntervalCount = 0
let continueIntent = true
const abort = () => {
// When request is cancelled, promise keeps pending
abortRequest && abortRequest(false)
continueIntent = false
}
const prom = new Promise((resolve, reject) => {
const exec = async () => {
intents++
const controller = new AbortController()
const timeout = setTimeout(controller.abort, config.timeout || DEFAULT_TIMEOUT_MS)
abortRequest = (retry = true) => {
clearTimeout(timeout)
clearTimeout(intentTimeout)
controller.abort()
if (retry) {
exec()
}
}
const { endpoint, uri, body, query, headers, method } = config
const options = {
method,
headers,
body: rawBody ? body : JSON.stringify(body),
signal: controller.signal,
...extraFetchOptions,
}
if (config.useAuthorization) {
const authString = await config.useAuthorization()
options.headers.Authorization = authString
}
try {
const endpointEndSlash = endpoint.endsWith('/')
const url = `${endpoint}${(uri && endpointEndSlash) || endpointEndSlash ? '' : '/'}${
uri.startsWith('/') ? uri.substr(1) : uri
}${query.length ? '?' : ''}${encodeURI(query.join('&'))}`
const result = await fetch(url, options)
if (rawResponse) {
resolve(result)
} else {
if (result.ok) {
try {
result.json().then(resolve)
} catch (e) {
resolve(result)
}
} else {
try {
result.json().then(reject)
} catch (e) {
reject(result)
}
}
}
} catch (e) {
if (config.maxIntents === 1) {
return reject(e)
} else if (intents > config.maxIntents) {
const error = new Error('Max failed intents')
return reject(error)
}
if (intentIntervalCount === RETRY_INTERVALS.length) {
intentIntervalCount = 0
} else {
intentIntervalCount++
}
if (continueIntent) {
intentTimeout = setTimeout(exec, RETRY_INTERVALS[intentIntervalCount] * 1000)
}
} finally {
clearTimeout(timeout)
}
}
exec()
})
prom.abort = abort
return prom
}
const obj = {
uri(uri = '/') {
config.uri = uri
return this
},
endpoint(endpoint) {
config.endpoint = endpoint
return this
},
params(params = {}) {
if (Array.isArray(params)) {
// ['a=1','b=2']
config.query = [...config.query, ...params]
} else {
// {a:1, b:2}
Object.keys(params).forEach(param => {
config.query.push(`${param}=${params[param]}`)
})
}
return this
},
useAuthorization(useAuthorization) {
// Specially used when many retrys are performed, token may expire
// expected response "Bearer <token>"
config.useAuthorization = useAuthorization
return this
},
maxIntents(maxIntents = 1) {
config.maxIntents = maxIntents > 0 ? maxIntents : 1
return this
},
headers(headers = {}) {
config.headers = { ...config.headers, ...headers }
return this
},
get() {
return doRequest()
},
post(body) {
config.method = 'POST'
config.body = body
return doRequest()
},
patch(body) {
config.method = 'PATCH'
config.body = body
return doRequest()
},
delete() {
config.method = 'DELETE'
return doRequest()
},
}
return obj
}
export default fetchAPI
@adway94
Copy link

adway94 commented May 19, 2021

Amazing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment