Hi @OndejMedek ,
I just wrote a little NodeJS code to check if my pseudo code makes sense.
const fetch = require('node-fetch');
// See also https://developer.atlassian.com/cloud/jira/platform/rate-limiting
const defaultRetryOptions = {
maxRetries: 4,
lastRetryDelayMillis: 5000,
maxRetryDelayMillis: 30000,
jitterMultiplierRange: [0.7, 1.3]
}
const apiFetch = {};
/**
* This method mimics node-fetch, but allows certain options to be passed in to
* control how to handle errors.
* @param {*} url the URL to be called.
* @param {*} fetchOptions the options as per node-fetch
* @param {*} retryOptions an object comprising fields maxRetries, lastRetryDelayMillis,
* maxRetryDelayMillis and jitterMultiplierRange. See defaultRetryOptions above.
* @returns the response of the API call.
*/
apiFetch.fetch = async function (url, fetchOptions, retryOptions) {
const retryCount = 0;
retryOptions = apiFetch._sanitiseRetryOptions(retryOptions);
return await apiFetch._fetch(url, fetchOptions, retryOptions, retryCount);
}
apiFetch._fetch = function (url, fetchOptions, retryOptions, retryCount) {
return new Promise(async (resolve, reject) => {
try {
const response = await fetch(url, fetchOptions);
if (response.ok) {
resolve(response);
} else {
let retryDelayMillis = -1;
let retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
retryDelayMillis = 1000 * parseInt(retryAfter);
console.log(`* Retry-After header value = ${retryAfter}, retryDelayMillis = ${retryDelayMillis}, retryCount = ${retryCount}`);
} else if (response.status === 429) {
retryDelayMillis = Math.min(2 * retryOptions.lastRetryDelayMillis, retryOptions.maxRetryDelayMillis);
console.log(`* Rate limited without Retry-After header! retryDelayMillis = ${retryDelayMillis}, retryCount = ${retryCount}`);
}
if (retryDelayMillis > 0 && retryCount < retryOptions.maxRetries) {
retryDelayMillis += retryDelayMillis * apiFetch._randomInRange(retryOptions.jitterMultiplierRange[0], retryOptions.jitterMultiplierRange[1]);
await apiFetch._delay(retryDelayMillis);
resolve(apiFetch._fetch(url, fetchOptions, retryOptions, retryCount + 1));
} else {
console.log(`Giving up and sending response with status ${response.status} (retry count = ${retryCount})...`);
resolve(response);
}
}
} catch (error) {
console.log(`Caught error:`, error);
reject(error);
}
});
}
apiFetch._delay = async function (millis) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, millis);
});
}
apiFetch._sanitiseRetryOptions = function (retryOptions) {
retryOptions = retryOptions ? retryOptions : {};
const sanitisedOptions = Object.assign({}, retryOptions, defaultRetryOptions);
if (sanitisedOptions.jitterMultiplierRange[1] <= sanitisedOptions.jitterMultiplierRange[0]) {
throw new Error(`jitterMultiplierRange must be an array with the second number beiong larger than the first.`);
}
return sanitisedOptions;
}
apiFetch._randomInRange = function (min, max) {
return Math.random() * (max - min) + min;
}
module.exports = apiFetch;
I gave it a little test and it seems to work, but definitely do additional testing. I hope it helps.
Regards,
Dugald