retry requests

Signed-off-by: Frank Jogeleit <frank.jogeleit@lovoo.com>
This commit is contained in:
Frank Jogeleit 2023-03-06 16:07:02 +01:00
parent 5f7d5f7c54
commit 2e2dec74b5
7 changed files with 344 additions and 129 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
node_modules node_modules
.vscode

View file

@ -43,6 +43,8 @@ jobs:
|ignoreStatusCodes| Prevent this Action to fail if the request respond with one of the configured Status Codes. Example: '404,401' || |ignoreStatusCodes| Prevent this Action to fail if the request respond with one of the configured Status Codes. Example: '404,401' ||
|httpsCA| Certificate authority as string in PEM format || |httpsCA| Certificate authority as string in PEM format ||
|responseFile| Persist the response data to the specified file path || |responseFile| Persist the response data to the specified file path ||
|retry| optional amount of retries if the request is failing, does not retry if the status code is ignored ||
|retryWait| time between each retry in millseconds | 3000 |
### Response ### Response

View file

@ -53,6 +53,12 @@ inputs:
responseFile: responseFile:
description: 'Persist the response data to the specified file path' description: 'Persist the response data to the specified file path'
required: false required: false
retry:
description: 'optional amount of retries if the request fails'
required: false
retryWait:
description: 'wait time between retries in milliseconds'
required: false
outputs: outputs:
response: response:
description: 'HTTP Response Content' description: 'HTTP Response Content'

273
dist/index.js vendored
View file

@ -5056,6 +5056,91 @@ const createPersistHandler = (filePath, actions) => (response) => {
module.exports = { createPersistHandler } module.exports = { createPersistHandler }
/***/ }),
/***/ 6989:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
"use strict";
const { GithubActions } = __nccwpck_require__(8169);
const FormData = __nccwpck_require__(4334);
const fs = __nccwpck_require__(7147);
/**
* @param {string} value
*
* @returns {Object}
*/
const convertToJSON = (value) => {
try {
return JSON.parse(value) || {};
} catch (e) {
return {};
}
};
/**
* @param {{ [key: string]: string }} data
* @param {{ [key: string]: string }} files
* @param {boolean} convertPaths
*
* @returns {FormData}
*/
const convertToFormData = (data, files, convertPaths) => {
const formData = new FormData();
for (const [key, value] of Object.entries(data)) {
formData.append(key, value);
}
for (const [key, value] of Object.entries(files)) {
formData.append(key, fs.createReadStream(value));
}
return formData;
};
/**
* @param {() => Promise} callback
* @param {{ retry: number; sleep: number; actions: GithubActions }} options
*
* @returns {Promise}
*/
const retry = async (callback, options) => {
let lastErr = null;
let i = 0;
do {
try {
return await callback();
} catch (err) {
lastErr = err;
}
if (i < options.retries) {
options.actions.warning(`#${i + 1} request failed: ${err}`);
await sleep(options.sleep);
}
i++;
} while (i <= options.retry);
throw lastErr;
};
function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
module.exports = {
convertToJSON,
convertToFormData,
retry,
};
/***/ }), /***/ }),
/***/ 9082: /***/ 9082:
@ -5065,10 +5150,11 @@ module.exports = { createPersistHandler }
const axios = __nccwpck_require__(8757); const axios = __nccwpck_require__(8757);
const FormData = __nccwpck_require__(4334) const FormData = __nccwpck_require__(4334);
const fs = __nccwpck_require__(7147) const fs = __nccwpck_require__(7147);
const url = __nccwpck_require__(7310); const url = __nccwpck_require__(7310);
const { GithubActions } = __nccwpck_require__(8169); const { GithubActions } = __nccwpck_require__(8169);
const { convertToJSON, convertToFormData, retry } = __nccwpck_require__(6989);
const METHOD_GET = 'GET' const METHOD_GET = 'GET'
const METHOD_POST = 'POST' const METHOD_POST = 'POST'
@ -5085,15 +5171,21 @@ const CONTENT_TYPE_URLENCODED = 'application/x-www-form-urlencoded'
* @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {} * @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {}
* @param {string} param0.file Single request file (absolute path) * @param {string} param0.file Single request file (absolute path)
* @param {GithubActions} param0.actions * @param {GithubActions} param0.actions
* @param {number[]} param0.ignoredCodes Prevent Action to fail if the API response with one of this StatusCodes * @param {{
* @param {boolean} param0.preventFailureOnNoResponse Prevent Action to fail if the API respond without Response * ignoredCodes: number[];
* @param {boolean} param0.escapeData Escape unescaped JSON content in data * preventFailureOnNoResponse: boolean,
* escapeData: boolean;
* retry: number;
* retryWait: number;
* }} param0.options
* *
* @returns {Promise<axios.AxiosResponse>} * @returns {Promise<axios.AxiosResponse>}
*/ */
const request = async({ method, instanceConfig, data, files, file, actions, ignoredCodes, preventFailureOnNoResponse, escapeData }) => { const request = async({ method, instanceConfig, data, files, file, actions, options }) => {
actions.debug(`options: ${JSON.stringify(options)}`)
try { try {
if (escapeData) { if (options.escapeData) {
data = data.replace(/"[^"]*"/g, (match) => { data = data.replace(/"[^"]*"/g, (match) => {
return match.replace(/[\n\r]\s*/g, "\\n"); return match.replace(/[\n\r]\s*/g, "\\n");
}); });
@ -5145,10 +5237,38 @@ const request = async({ method, instanceConfig, data, files, file, actions, igno
actions.debug('Request Data: ' + JSON.stringify(requestData)) actions.debug('Request Data: ' + JSON.stringify(requestData))
const response = await instance.request(requestData) const execRequest = async () => {
try {
return await instance.request(requestData)
} catch(error) {
if (error.response && options.ignoredCodes.includes(error.response.status)) {
actions.warning(`ignored status code: ${JSON.stringify({ code: error.response.status, message: error.response.data })}`)
return null
}
if (!error.response && error.request && options.preventFailureOnNoResponse) {
actions.warning(`no response received: ${JSON.stringify(error)}`);
return null
}
throw error
}
}
/** @type {axios.AxiosResponse|null} */
const response = await retry(execRequest, {
actions,
retry: options.retry || 0,
sleep: options.retryWait // wait 3s after each retry
})
if (!response) {
return null
}
actions.setOutput('response', JSON.stringify(response.data)) actions.setOutput('response', JSON.stringify(response.data))
actions.setOutput('headers', response.headers) actions.setOutput('headers', response.headers)
return response return response
@ -5158,53 +5278,16 @@ const request = async({ method, instanceConfig, data, files, file, actions, igno
actions.setOutput('requestError', JSON.stringify({ name, message, code, status: response && response.status ? response.status : null })); actions.setOutput('requestError', JSON.stringify({ name, message, code, status: response && response.status ? response.status : null }));
} }
if (error.response && ignoredCodes.includes(error.response.status)) { if (error.response) {
actions.warning(JSON.stringify({ code: error.response.status, message: error.response.data }))
} else if (error.response) {
actions.setFailed(JSON.stringify({ code: error.response.status, message: error.response.data })) actions.setFailed(JSON.stringify({ code: error.response.status, message: error.response.data }))
} else if (error.request && !preventFailureOnNoResponse) { } else if (error.request) {
actions.setFailed(JSON.stringify({ error: "no response received" })); actions.setFailed(JSON.stringify({ error: "no response received" }));
} else if (error.request && preventFailureOnNoResponse) {
actions.warning(JSON.stringify(error));
} else { } else {
actions.setFailed(JSON.stringify({ message: error.message, data })); actions.setFailed(JSON.stringify({ message: error.message, data }));
} }
} }
} }
/**
* @param {string} value
*
* @returns {Object}
*/
const convertToJSON = (value) => {
try {
return JSON.parse(value) || {}
} catch(e) {
return {}
}
}
/**
* @param {Object} data
* @param {Object} files
*
* @returns {FormData}
*/
const convertToFormData = (data, files) => {
const formData = new FormData()
for (const [key, value] of Object.entries(data)) {
formData.append(key, value)
}
for (const [key, value] of Object.entries(files)) {
formData.append(key, fs.createReadStream(value))
}
return formData
}
/** /**
* @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} instanceConfig * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} instanceConfig
* @param {FormData} formData * @param {FormData} formData
@ -5406,7 +5489,7 @@ module.exports = require("zlib");
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
"use strict"; "use strict";
// Axios v1.3.2 Copyright (c) 2023 Matt Zabriskie and contributors // Axios v1.3.4 Copyright (c) 2023 Matt Zabriskie and contributors
const FormData$1 = __nccwpck_require__(4334); const FormData$1 = __nccwpck_require__(4334);
@ -6989,11 +7072,15 @@ function isValidHeaderName(str) {
return /^[-_a-zA-Z]+$/.test(str.trim()); return /^[-_a-zA-Z]+$/.test(str.trim());
} }
function matchHeaderValue(context, value, header, filter) { function matchHeaderValue(context, value, header, filter, isHeaderNameFilter) {
if (utils.isFunction(filter)) { if (utils.isFunction(filter)) {
return filter.call(this, value, header); return filter.call(this, value, header);
} }
if (isHeaderNameFilter) {
value = header;
}
if (!utils.isString(value)) return; if (!utils.isString(value)) return;
if (utils.isString(filter)) { if (utils.isString(filter)) {
@ -7137,7 +7224,7 @@ class AxiosHeaders {
while (i--) { while (i--) {
const key = keys[i]; const key = keys[i];
if(!matcher || matchHeaderValue(this, this[key], key, matcher)) { if(!matcher || matchHeaderValue(this, this[key], key, matcher, true)) {
delete this[key]; delete this[key];
deleted = true; deleted = true;
} }
@ -7356,7 +7443,7 @@ function buildFullPath(baseURL, requestedURL) {
return requestedURL; return requestedURL;
} }
const VERSION = "1.3.2"; const VERSION = "1.3.4";
function parseProtocol(url) { function parseProtocol(url) {
const match = /^([-+\w]{1,25})(:?\/\/|:)/.exec(url); const match = /^([-+\w]{1,25})(:?\/\/|:)/.exec(url);
@ -7918,15 +8005,39 @@ function setProxy(options, configProxy, location) {
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process'; const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
// temporary hotfix
const wrapAsync = (asyncExecutor) => {
return new Promise((resolve, reject) => {
let onDone;
let isDone;
const done = (value, isRejected) => {
if (isDone) return;
isDone = true;
onDone && onDone(value, isRejected);
};
const _resolve = (value) => {
done(value);
resolve(value);
};
const _reject = (reason) => {
done(reason, true);
reject(reason);
};
asyncExecutor(_resolve, _reject, (onDoneHandler) => (onDone = onDoneHandler)).catch(_reject);
})
};
/*eslint consistent-return:0*/ /*eslint consistent-return:0*/
const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) { const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) {
/*eslint no-async-promise-executor:0*/ return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) {
return new Promise(async function dispatchHttpRequest(resolvePromise, rejectPromise) { let {data} = config;
let data = config.data; const {responseType, responseEncoding} = config;
const responseType = config.responseType;
const responseEncoding = config.responseEncoding;
const method = config.method.toUpperCase(); const method = config.method.toUpperCase();
let isFinished;
let isDone; let isDone;
let rejected = false; let rejected = false;
let req; let req;
@ -7934,10 +8045,7 @@ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) {
// temporary internal emitter until the AxiosRequest class will be implemented // temporary internal emitter until the AxiosRequest class will be implemented
const emitter = new EventEmitter__default["default"](); const emitter = new EventEmitter__default["default"]();
function onFinished() { const onFinished = () => {
if (isFinished) return;
isFinished = true;
if (config.cancelToken) { if (config.cancelToken) {
config.cancelToken.unsubscribe(abort); config.cancelToken.unsubscribe(abort);
} }
@ -7947,28 +8055,15 @@ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) {
} }
emitter.removeAllListeners(); emitter.removeAllListeners();
} };
function done(value, isRejected) {
if (isDone) return;
onDone((value, isRejected) => {
isDone = true; isDone = true;
if (isRejected) { if (isRejected) {
rejected = true; rejected = true;
onFinished(); onFinished();
} }
});
isRejected ? rejectPromise(value) : resolvePromise(value);
}
const resolve = function resolve(value) {
done(value);
};
const reject = function reject(value) {
done(value, true);
};
function abort(reason) { function abort(reason) {
emitter.emit('abort', !reason || reason.type ? new CanceledError(null, config, req) : reason); emitter.emit('abort', !reason || reason.type ? new CanceledError(null, config, req) : reason);
@ -8066,7 +8161,7 @@ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) {
if (!headers.hasContentLength()) { if (!headers.hasContentLength()) {
try { try {
const knownLength = await util__default["default"].promisify(data.getLength).call(data); const knownLength = await util__default["default"].promisify(data.getLength).call(data);
headers.setContentLength(knownLength); Number.isFinite(knownLength) && knownLength >= 0 && headers.setContentLength(knownLength);
/*eslint no-empty:0*/ /*eslint no-empty:0*/
} catch (e) { } catch (e) {
} }
@ -9683,6 +9778,16 @@ if (!!core.getInput('username') || !!core.getInput('password')) {
} }
} }
let retry = 0
if (!!core.getInput('retry')) {
retry = parseInt(core.getInput('retry'))
}
let retryWait = 3000
if (!!core.getInput('retryWait')) {
retry = parseInt(core.getInput('retryWait'))
}
const data = core.getInput('data') || '{}'; const data = core.getInput('data') || '{}';
const files = core.getInput('files') || '{}'; const files = core.getInput('files') || '{}';
const file = core.getInput('file') const file = core.getInput('file')
@ -9705,7 +9810,15 @@ if (!!responseFile) {
handler.push(createPersistHandler(responseFile, actions)) handler.push(createPersistHandler(responseFile, actions))
} }
request({ data, method, instanceConfig, preventFailureOnNoResponse, escapeData, files, file, ignoredCodes, actions }).then(response => { const options = {
ignoredCodes,
preventFailureOnNoResponse,
escapeData,
retry,
retryWait
}
request({ data, method, instanceConfig, files, file, actions, options }).then(response => {
if (typeof response == 'object') { if (typeof response == 'object') {
handler.forEach(h => h(response)) handler.forEach(h => h(response))
} }

77
src/helper.js Normal file
View file

@ -0,0 +1,77 @@
'use strict';
const { GithubActions } = require('./githubActions');
const FormData = require('form-data');
const fs = require('fs');
/**
* @param {string} value
*
* @returns {Object}
*/
const convertToJSON = (value) => {
try {
return JSON.parse(value) || {};
} catch (e) {
return {};
}
};
/**
* @param {{ [key: string]: string }} data
* @param {{ [key: string]: string }} files
* @param {boolean} convertPaths
*
* @returns {FormData}
*/
const convertToFormData = (data, files, convertPaths) => {
const formData = new FormData();
for (const [key, value] of Object.entries(data)) {
formData.append(key, value);
}
for (const [key, value] of Object.entries(files)) {
formData.append(key, fs.createReadStream(value));
}
return formData;
};
/**
* @param {() => Promise} callback
* @param {{ retry: number; sleep: number; actions: GithubActions }} options
*
* @returns {Promise}
*/
const retry = async (callback, options) => {
let lastErr = null;
let i = 0;
do {
try {
return await callback();
} catch (err) {
lastErr = err;
}
if (i < options.retries) {
options.actions.warning(`#${i + 1} request failed: ${err}`);
await sleep(options.sleep);
}
i++;
} while (i <= options.retry);
throw lastErr;
};
function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
module.exports = {
convertToJSON,
convertToFormData,
retry,
};

View file

@ -1,10 +1,11 @@
'use strict' 'use strict'
const axios = require('axios'); const axios = require('axios');
const FormData = require('form-data') const FormData = require('form-data');
const fs = require('fs') const fs = require('fs');
const url = require('url'); const url = require('url');
const { GithubActions } = require('./githubActions'); const { GithubActions } = require('./githubActions');
const { convertToJSON, convertToFormData, retry } = require('./helper');
const METHOD_GET = 'GET' const METHOD_GET = 'GET'
const METHOD_POST = 'POST' const METHOD_POST = 'POST'
@ -21,15 +22,21 @@ const CONTENT_TYPE_URLENCODED = 'application/x-www-form-urlencoded'
* @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {} * @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {}
* @param {string} param0.file Single request file (absolute path) * @param {string} param0.file Single request file (absolute path)
* @param {GithubActions} param0.actions * @param {GithubActions} param0.actions
* @param {number[]} param0.ignoredCodes Prevent Action to fail if the API response with one of this StatusCodes * @param {{
* @param {boolean} param0.preventFailureOnNoResponse Prevent Action to fail if the API respond without Response * ignoredCodes: number[];
* @param {boolean} param0.escapeData Escape unescaped JSON content in data * preventFailureOnNoResponse: boolean,
* escapeData: boolean;
* retry: number;
* retryWait: number;
* }} param0.options
* *
* @returns {Promise<axios.AxiosResponse>} * @returns {Promise<axios.AxiosResponse>}
*/ */
const request = async({ method, instanceConfig, data, files, file, actions, ignoredCodes, preventFailureOnNoResponse, escapeData }) => { const request = async({ method, instanceConfig, data, files, file, actions, options }) => {
actions.debug(`options: ${JSON.stringify(options)}`)
try { try {
if (escapeData) { if (options.escapeData) {
data = data.replace(/"[^"]*"/g, (match) => { data = data.replace(/"[^"]*"/g, (match) => {
return match.replace(/[\n\r]\s*/g, "\\n"); return match.replace(/[\n\r]\s*/g, "\\n");
}); });
@ -81,10 +88,38 @@ const request = async({ method, instanceConfig, data, files, file, actions, igno
actions.debug('Request Data: ' + JSON.stringify(requestData)) actions.debug('Request Data: ' + JSON.stringify(requestData))
const response = await instance.request(requestData) const execRequest = async () => {
try {
return await instance.request(requestData)
} catch(error) {
if (error.response && options.ignoredCodes.includes(error.response.status)) {
actions.warning(`ignored status code: ${JSON.stringify({ code: error.response.status, message: error.response.data })}`)
return null
}
if (!error.response && error.request && options.preventFailureOnNoResponse) {
actions.warning(`no response received: ${JSON.stringify(error)}`);
return null
}
throw error
}
}
/** @type {axios.AxiosResponse|null} */
const response = await retry(execRequest, {
actions,
retry: options.retry || 0,
sleep: options.retryWait // wait 3s after each retry
})
if (!response) {
return null
}
actions.setOutput('response', JSON.stringify(response.data)) actions.setOutput('response', JSON.stringify(response.data))
actions.setOutput('headers', response.headers) actions.setOutput('headers', response.headers)
return response return response
@ -94,53 +129,16 @@ const request = async({ method, instanceConfig, data, files, file, actions, igno
actions.setOutput('requestError', JSON.stringify({ name, message, code, status: response && response.status ? response.status : null })); actions.setOutput('requestError', JSON.stringify({ name, message, code, status: response && response.status ? response.status : null }));
} }
if (error.response && ignoredCodes.includes(error.response.status)) { if (error.response) {
actions.warning(JSON.stringify({ code: error.response.status, message: error.response.data }))
} else if (error.response) {
actions.setFailed(JSON.stringify({ code: error.response.status, message: error.response.data })) actions.setFailed(JSON.stringify({ code: error.response.status, message: error.response.data }))
} else if (error.request && !preventFailureOnNoResponse) { } else if (error.request) {
actions.setFailed(JSON.stringify({ error: "no response received" })); actions.setFailed(JSON.stringify({ error: "no response received" }));
} else if (error.request && preventFailureOnNoResponse) {
actions.warning(JSON.stringify(error));
} else { } else {
actions.setFailed(JSON.stringify({ message: error.message, data })); actions.setFailed(JSON.stringify({ message: error.message, data }));
} }
} }
} }
/**
* @param {string} value
*
* @returns {Object}
*/
const convertToJSON = (value) => {
try {
return JSON.parse(value) || {}
} catch(e) {
return {}
}
}
/**
* @param {Object} data
* @param {Object} files
*
* @returns {FormData}
*/
const convertToFormData = (data, files) => {
const formData = new FormData()
for (const [key, value] of Object.entries(data)) {
formData.append(key, value)
}
for (const [key, value] of Object.entries(files)) {
formData.append(key, fs.createReadStream(value))
}
return formData
}
/** /**
* @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} instanceConfig * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} instanceConfig
* @param {FormData} formData * @param {FormData} formData

View file

@ -43,6 +43,16 @@ if (!!core.getInput('username') || !!core.getInput('password')) {
} }
} }
let retry = 0
if (!!core.getInput('retry')) {
retry = parseInt(core.getInput('retry'))
}
let retryWait = 3000
if (!!core.getInput('retryWait')) {
retry = parseInt(core.getInput('retryWait'))
}
const data = core.getInput('data') || '{}'; const data = core.getInput('data') || '{}';
const files = core.getInput('files') || '{}'; const files = core.getInput('files') || '{}';
const file = core.getInput('file') const file = core.getInput('file')
@ -65,7 +75,15 @@ if (!!responseFile) {
handler.push(createPersistHandler(responseFile, actions)) handler.push(createPersistHandler(responseFile, actions))
} }
request({ data, method, instanceConfig, preventFailureOnNoResponse, escapeData, files, file, ignoredCodes, actions }).then(response => { const options = {
ignoredCodes,
preventFailureOnNoResponse,
escapeData,
retry,
retryWait
}
request({ data, method, instanceConfig, files, file, actions, options }).then(response => {
if (typeof response == 'object') { if (typeof response == 'object') {
handler.forEach(h => h(response)) handler.forEach(h => h(response))
} }