From edf33a9d70a10bad67ea1ef8abfa864ab68563e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Feb 2023 17:56:36 +0000 Subject: [PATCH 1/5] Bump axios from 1.3.2 to 1.3.3 Bumps [axios](https://github.com/axios/axios) from 1.3.2 to 1.3.3. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.3.2...v1.3.3) --- updated-dependencies: - dependency-name: axios dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index f32f5a6..65744e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@vercel/ncc": "^0.36.1", - "axios": "^1.2", + "axios": "^1.3", "form-data": "^4.0.0", "yargs": "^17.6.2" }, @@ -78,9 +78,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.2.tgz", - "integrity": "sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.3.tgz", + "integrity": "sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==", "dev": true, "dependencies": { "follow-redirects": "^1.15.0", @@ -386,9 +386,9 @@ "dev": true }, "axios": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.2.tgz", - "integrity": "sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.3.tgz", + "integrity": "sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==", "dev": true, "requires": { "follow-redirects": "^1.15.0", diff --git a/package.json b/package.json index 9b4936a..8523bb5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "homepage": "https://github.com/fjogeleit/http-request-action#readme", "devDependencies": { "@vercel/ncc": "^0.36.1", - "axios": "^1.2", + "axios": "^1.3", "form-data": "^4.0.0", "yargs": "^17.6.2" }, From 12f71e545a5c5fee71c081621e57823466dbc561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Feb 2023 17:56:42 +0000 Subject: [PATCH 2/5] Bump yargs from 17.6.2 to 17.7.0 Bumps [yargs](https://github.com/yargs/yargs) from 17.6.2 to 17.7.0. - [Release notes](https://github.com/yargs/yargs/releases) - [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md) - [Commits](https://github.com/yargs/yargs/compare/v17.6.2...v17.7.0) --- updated-dependencies: - dependency-name: yargs dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index f32f5a6..b99c809 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@vercel/ncc": "^0.36.1", "axios": "^1.2", "form-data": "^4.0.0", - "yargs": "^17.6.2" + "yargs": "^17.7.0" }, "engines": { "node": ">=16.0.0" @@ -313,9 +313,9 @@ } }, "node_modules/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.0.tgz", + "integrity": "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -553,9 +553,9 @@ "dev": true }, "yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.0.tgz", + "integrity": "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==", "dev": true, "requires": { "cliui": "^8.0.1", diff --git a/package.json b/package.json index 9b4936a..68891bb 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@vercel/ncc": "^0.36.1", "axios": "^1.2", "form-data": "^4.0.0", - "yargs": "^17.6.2" + "yargs": "^17.7.0" }, "dependencies": { "@actions/core": "^1.10.0" From 81ecdf1750f965524d94e1b3e4f5095fdcd6f55e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 17:56:32 +0000 Subject: [PATCH 3/5] Bump axios from 1.3.3 to 1.3.4 Bumps [axios](https://github.com/axios/axios) from 1.3.3 to 1.3.4. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.3.3...v1.3.4) --- updated-dependencies: - dependency-name: axios dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8745d15..61bca55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,9 +78,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.3.tgz", - "integrity": "sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", "dev": true, "dependencies": { "follow-redirects": "^1.15.0", @@ -386,9 +386,9 @@ "dev": true }, "axios": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.3.tgz", - "integrity": "sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", "dev": true, "requires": { "follow-redirects": "^1.15.0", From 5500a62817ccda80b1201440e81dc815233eceb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 17:56:43 +0000 Subject: [PATCH 4/5] Bump yargs from 17.7.0 to 17.7.1 Bumps [yargs](https://github.com/yargs/yargs) from 17.7.0 to 17.7.1. - [Release notes](https://github.com/yargs/yargs/releases) - [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md) - [Commits](https://github.com/yargs/yargs/compare/v17.7.0...v17.7.1) --- updated-dependencies: - dependency-name: yargs dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8745d15..11a4829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@vercel/ncc": "^0.36.1", "axios": "^1.3", "form-data": "^4.0.0", - "yargs": "^17.7.0" + "yargs": "^17.7.1" }, "engines": { "node": ">=16.0.0" @@ -313,9 +313,9 @@ } }, "node_modules/yargs": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.0.tgz", - "integrity": "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==", + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -553,9 +553,9 @@ "dev": true }, "yargs": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.0.tgz", - "integrity": "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==", + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", "dev": true, "requires": { "cliui": "^8.0.1", diff --git a/package.json b/package.json index 6fcd851..5392023 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@vercel/ncc": "^0.36.1", "axios": "^1.3", "form-data": "^4.0.0", - "yargs": "^17.7.0" + "yargs": "^17.7.1" }, "dependencies": { "@actions/core": "^1.10.0" From 2e2dec74b5eec5e974d9fc952854ebd0c8d37b8c Mon Sep 17 00:00:00 2001 From: Frank Jogeleit Date: Mon, 6 Mar 2023 16:07:02 +0100 Subject: [PATCH 5/5] retry requests Signed-off-by: Frank Jogeleit --- .gitignore | 1 + README.md | 2 + action.yml | 6 + dist/index.js | 273 ++++++++++++++++++++++++++++++++-------------- src/helper.js | 77 +++++++++++++ src/httpClient.js | 94 ++++++++-------- src/index.js | 20 +++- 7 files changed, 344 insertions(+), 129 deletions(-) create mode 100644 src/helper.js diff --git a/.gitignore b/.gitignore index 3c3629e..55371e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 62c7acf..f190926 100644 --- a/README.md +++ b/README.md @@ -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' || |httpsCA| Certificate authority as string in PEM format || |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 diff --git a/action.yml b/action.yml index ce0c06f..a7f1775 100644 --- a/action.yml +++ b/action.yml @@ -53,6 +53,12 @@ inputs: responseFile: description: 'Persist the response data to the specified file path' 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: response: description: 'HTTP Response Content' diff --git a/dist/index.js b/dist/index.js index b727ab5..d2eb978 100644 --- a/dist/index.js +++ b/dist/index.js @@ -5056,6 +5056,91 @@ const createPersistHandler = (filePath, actions) => (response) => { 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: @@ -5065,10 +5150,11 @@ module.exports = { createPersistHandler } const axios = __nccwpck_require__(8757); -const FormData = __nccwpck_require__(4334) -const fs = __nccwpck_require__(7147) +const FormData = __nccwpck_require__(4334); +const fs = __nccwpck_require__(7147); const url = __nccwpck_require__(7310); const { GithubActions } = __nccwpck_require__(8169); +const { convertToJSON, convertToFormData, retry } = __nccwpck_require__(6989); const METHOD_GET = 'GET' 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.file Single request file (absolute path) * @param {GithubActions} param0.actions - * @param {number[]} param0.ignoredCodes Prevent Action to fail if the API response with one of this StatusCodes - * @param {boolean} param0.preventFailureOnNoResponse Prevent Action to fail if the API respond without Response - * @param {boolean} param0.escapeData Escape unescaped JSON content in data + * @param {{ + * ignoredCodes: number[]; + * preventFailureOnNoResponse: boolean, + * escapeData: boolean; + * retry: number; + * retryWait: number; + * }} param0.options * * @returns {Promise} */ -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 { - if (escapeData) { + if (options.escapeData) { data = data.replace(/"[^"]*"/g, (match) => { 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)) - 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('headers', response.headers) 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 })); } - if (error.response && ignoredCodes.includes(error.response.status)) { - actions.warning(JSON.stringify({ code: error.response.status, message: error.response.data })) - } else if (error.response) { + if (error.response) { 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" })); - } else if (error.request && preventFailureOnNoResponse) { - actions.warning(JSON.stringify(error)); } else { 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 {FormData} formData @@ -5406,7 +5489,7 @@ module.exports = require("zlib"); /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "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); @@ -6989,11 +7072,15 @@ function isValidHeaderName(str) { return /^[-_a-zA-Z]+$/.test(str.trim()); } -function matchHeaderValue(context, value, header, filter) { +function matchHeaderValue(context, value, header, filter, isHeaderNameFilter) { if (utils.isFunction(filter)) { return filter.call(this, value, header); } + if (isHeaderNameFilter) { + value = header; + } + if (!utils.isString(value)) return; if (utils.isString(filter)) { @@ -7137,7 +7224,7 @@ class AxiosHeaders { while (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]; deleted = true; } @@ -7356,7 +7443,7 @@ function buildFullPath(baseURL, requestedURL) { return requestedURL; } -const VERSION = "1.3.2"; +const VERSION = "1.3.4"; function parseProtocol(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'; +// 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*/ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) { - /*eslint no-async-promise-executor:0*/ - return new Promise(async function dispatchHttpRequest(resolvePromise, rejectPromise) { - let data = config.data; - const responseType = config.responseType; - const responseEncoding = config.responseEncoding; + return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) { + let {data} = config; + const {responseType, responseEncoding} = config; const method = config.method.toUpperCase(); - let isFinished; let isDone; let rejected = false; let req; @@ -7934,10 +8045,7 @@ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) { // temporary internal emitter until the AxiosRequest class will be implemented const emitter = new EventEmitter__default["default"](); - function onFinished() { - if (isFinished) return; - isFinished = true; - + const onFinished = () => { if (config.cancelToken) { config.cancelToken.unsubscribe(abort); } @@ -7947,28 +8055,15 @@ const httpAdapter = isHttpAdapterSupported && function httpAdapter(config) { } emitter.removeAllListeners(); - } - - function done(value, isRejected) { - if (isDone) return; + }; + onDone((value, isRejected) => { isDone = true; - if (isRejected) { rejected = true; onFinished(); } - - isRejected ? rejectPromise(value) : resolvePromise(value); - } - - const resolve = function resolve(value) { - done(value); - }; - - const reject = function reject(value) { - done(value, true); - }; + }); function abort(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()) { try { 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*/ } 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 files = core.getInput('files') || '{}'; const file = core.getInput('file') @@ -9705,7 +9810,15 @@ if (!!responseFile) { 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') { handler.forEach(h => h(response)) } diff --git a/src/helper.js b/src/helper.js new file mode 100644 index 0000000..5144de2 --- /dev/null +++ b/src/helper.js @@ -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, +}; diff --git a/src/httpClient.js b/src/httpClient.js index bea553c..fa5cc17 100644 --- a/src/httpClient.js +++ b/src/httpClient.js @@ -1,10 +1,11 @@ 'use strict' const axios = require('axios'); -const FormData = require('form-data') -const fs = require('fs') +const FormData = require('form-data'); +const fs = require('fs'); const url = require('url'); const { GithubActions } = require('./githubActions'); +const { convertToJSON, convertToFormData, retry } = require('./helper'); const METHOD_GET = 'GET' 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.file Single request file (absolute path) * @param {GithubActions} param0.actions - * @param {number[]} param0.ignoredCodes Prevent Action to fail if the API response with one of this StatusCodes - * @param {boolean} param0.preventFailureOnNoResponse Prevent Action to fail if the API respond without Response - * @param {boolean} param0.escapeData Escape unescaped JSON content in data + * @param {{ + * ignoredCodes: number[]; + * preventFailureOnNoResponse: boolean, + * escapeData: boolean; + * retry: number; + * retryWait: number; + * }} param0.options * * @returns {Promise} */ -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 { - if (escapeData) { + if (options.escapeData) { data = data.replace(/"[^"]*"/g, (match) => { 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)) - 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('headers', response.headers) 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 })); } - if (error.response && ignoredCodes.includes(error.response.status)) { - actions.warning(JSON.stringify({ code: error.response.status, message: error.response.data })) - } else if (error.response) { + if (error.response) { 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" })); - } else if (error.request && preventFailureOnNoResponse) { - actions.warning(JSON.stringify(error)); } else { 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 {FormData} formData diff --git a/src/index.js b/src/index.js index af722ba..75147d9 100644 --- a/src/index.js +++ b/src/index.js @@ -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 files = core.getInput('files') || '{}'; const file = core.getInput('file') @@ -65,7 +75,15 @@ if (!!responseFile) { 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') { handler.forEach(h => h(response)) }