diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20e6e35..4003bed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,13 @@ jobs: username: 'postman' password: 'password' + - name: Request Postment Echo with 404 Response and ignore failure code + uses: ./ + with: + url: 'https://postman-echo.com/status/404' + method: 'GET' + ignoreStatusCodes: '404' + - name: Create Test File id: image run: | diff --git a/README.md b/README.md index c8722aa..18f63dc 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ jobs: |password| Password for Basic Auth || |bearerToken| Bearer Authentication Token (without Bearer Prefix) || |customHeaders| Additional header values as JSON string, keys in this object overwrite default headers like Content-Type |'{}'| -|preventFailureOnNoResponse| Prevent this Action to fail if the request respond without an response. Use 'true' (string) as value to enable it || |escapeData| Escape newlines in data string content. Use 'true' (string) as value to enable it || +|preventFailureOnNoResponse| Prevent this Action to fail if the request respond without an response. Use 'true' (string) as value to enable it || +|ignoreStatusCodes| Prevent this Action to fail if the request respond with one of the configured Status Codes. Example: '404,401' || ### Response diff --git a/action.yml b/action.yml index 0f306b3..6faad0e 100644 --- a/action.yml +++ b/action.yml @@ -38,6 +38,9 @@ inputs: preventFailureOnNoResponse: description: 'Prevent this Action to fail if the request respond without an response' required: false + ignoreStatusCodes: + description: 'Prevent this Action to fail if the request respond with one of the configured StatusCodes' + required: false escapeData: description: 'Escape newlines in data string content' required: false diff --git a/dist/index.js b/dist/index.js index 8af8fbd..69bc611 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1340,7 +1340,21 @@ const fs = __webpack_require__(747) const METHOD_GET = 'GET' const METHOD_POST = 'POST' -const request = async({ method, instanceConfig, data, files, auth, actions, preventFailureOnNoResponse, escapeData }) => { +/** + * @param {Object} param0 + * @param {string} param0.method HTTP Method + * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} param0.instanceConfig + * @param {string} param0.data Request Body as string, default {} + * @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {} + * @param {{ username: string; password: string }|undefined} param0.auth Optional HTTP Basic Auth + * @param {*} 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 + * + * @returns {void} + */ +const request = async({ method, instanceConfig, data, files, auth, actions, ignoredCodes, preventFailureOnNoResponse, escapeData }) => { try { if (escapeData) { data = data.replace(/"[^"]*"/g, (match) => { @@ -1386,11 +1400,13 @@ const request = async({ method, instanceConfig, data, files, auth, actions, prev actions.setOutput('response', JSON.stringify(response.data)) } catch (error) { if (error.toJSON) { - actions.setOutput(JSON.stringify(error.toJSON())); + actions.setOutput('requestError', JSON.stringify(error.toJSON())); } - if (error.response) { - actions.setFailed(JSON.stringify({ code: error.response.code, message: error.response.data })) + if (error.response && ignoredCodes.includes(error.response.status)) { + 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 })) } else if (error.request && !preventFailureOnNoResponse) { actions.setFailed(JSON.stringify({ error: "no response received" })); } else if (error.request && preventFailureOnNoResponse) { @@ -1401,6 +1417,11 @@ const request = async({ method, instanceConfig, data, files, auth, actions, prev } } +/** + * @param {string} value + * + * @returns {Object} + */ const convertToJSON = (value) => { try { return JSON.parse(value) @@ -1409,6 +1430,12 @@ const convertToJSON = (value) => { } } +/** + * @param {Object} data + * @param {Object} files + * + * @returns {FormData} + */ const convertToFormData = (data, files) => { formData = new FormData() @@ -1423,6 +1450,13 @@ const convertToFormData = (data, files) => { return formData } +/** + * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} instanceConfig + * @param {FormData} formData + * @param {*} actions + * + * @returns {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} + */ const updateConfig = async (instanceConfig, formData, actions) => { try { const formHeaders = formData.getHeaders() @@ -1444,6 +1478,11 @@ const updateConfig = async (instanceConfig, formData, actions) => { } } +/** + * @param {FormData} formData + * + * @returns {Promise} + */ const contentLength = (formData) => new Promise((resolve, reject) => { formData.getLength((err, length) => { if (err) { @@ -1891,14 +1930,19 @@ function populateMaps (extensions, types) { /***/ (function(module, __unusedexports, __webpack_require__) { var debug; -try { - /* eslint global-require: off */ - debug = __webpack_require__(944)("follow-redirects"); -} -catch (error) { - debug = function () { /* */ }; -} -module.exports = debug; + +module.exports = function () { + if (!debug) { + try { + /* eslint global-require: off */ + debug = __webpack_require__(944)("follow-redirects"); + } + catch (error) { + debug = function () { /* */ }; + } + } + debug.apply(null, arguments); +}; /***/ }), @@ -2575,6 +2619,17 @@ function RedirectableRequest(options, responseCallback) { } RedirectableRequest.prototype = Object.create(Writable.prototype); +RedirectableRequest.prototype.abort = function () { + // Abort the internal request + this._currentRequest.removeAllListeners(); + this._currentRequest.on("error", noop); + this._currentRequest.abort(); + + // Abort this request + this.emit("abort"); + this.removeAllListeners(); +}; + // Writes buffered data to the current native request RedirectableRequest.prototype.write = function (data, encoding, callback) { // Writing is not allowed if end has been called @@ -2654,18 +2709,39 @@ RedirectableRequest.prototype.removeHeader = function (name) { // Global timeout for all underlying requests RedirectableRequest.prototype.setTimeout = function (msecs, callback) { + var self = this; if (callback) { - this.once("timeout", callback); + this.on("timeout", callback); } + // Sets up a timer to trigger a timeout event + function startTimer() { + if (self._timeout) { + clearTimeout(self._timeout); + } + self._timeout = setTimeout(function () { + self.emit("timeout"); + clearTimer(); + }, msecs); + } + + // Prevent a timeout from triggering + function clearTimer() { + clearTimeout(this._timeout); + if (callback) { + self.removeListener("timeout", callback); + } + if (!this.socket) { + self._currentRequest.removeListener("socket", startTimer); + } + } + + // Start the timer when the socket is opened if (this.socket) { - startTimer(this, msecs); + startTimer(); } else { - var self = this; - this._currentRequest.once("socket", function () { - startTimer(self, msecs); - }); + this._currentRequest.once("socket", startTimer); } this.once("response", clearTimer); @@ -2674,20 +2750,9 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) { return this; }; -function startTimer(request, msecs) { - clearTimeout(request._timeout); - request._timeout = setTimeout(function () { - request.emit("timeout"); - }, msecs); -} - -function clearTimer() { - clearTimeout(this._timeout); -} - // Proxy all other public ClientRequest methods [ - "abort", "flushHeaders", "getHeader", + "flushHeaders", "getHeader", "setNoDelay", "setSocketKeepAlive", ].forEach(function (method) { RedirectableRequest.prototype[method] = function (a, b) { @@ -3577,7 +3642,14 @@ const method = core.getInput('method') || METHOD_POST; const preventFailureOnNoResponse = core.getInput('preventFailureOnNoResponse') === 'true'; const escapeData = core.getInput('escapeData') === 'true'; -request({ data, method, instanceConfig, auth, preventFailureOnNoResponse, escapeData, files, actions: new GithubActions() }) +const ignoreStatusCodes = core.getInput('ignoreStatusCodes') +let ignoredCodes = [] + +if (typeof ignoreStatusCodes === 'string' && ignoreStatusCodes.length > 0) { + ignoredCodes = ignoreStatusCodes.split(',').map(statusCode => parseInt(statusCode.trim())) +} + +request({ data, method, instanceConfig, auth, preventFailureOnNoResponse, escapeData, files, ignoredCodes, actions: new GithubActions() }) /***/ }), diff --git a/package-lock.json b/package-lock.json index 92369e7..cc5b825 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,120 @@ { "name": "http-request-action", - "version": "1.7.3", - "lockfileVersion": 1, + "version": "1.8.0", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "version": "1.8.0", + "license": "MIT", + "dependencies": { + "@zeit/ncc": "^0.22", + "axios": "^0.21.1", + "form-data": "^3.0.1" + }, + "devDependencies": { + "@actions/core": "^1.2.6" + } + }, + "node_modules/@actions/core": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", + "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==", + "dev": true + }, + "node_modules/@zeit/ncc": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.22.3.tgz", + "integrity": "sha512-jnCLpLXWuw/PAiJiVbLjA8WBC0IJQbFeUwF4I9M+23MvIxTxk5pD4Q8byQBSPmHQjz5aBoA7AKAElQxMpjrCLQ==", + "deprecated": "@zeit/ncc is no longer maintained. Please use @vercel/ncc instead.", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", + "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", + "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", + "dependencies": { + "mime-db": "1.46.0" + }, + "engines": { + "node": ">= 0.6" + } + } + }, "dependencies": { "@actions/core": { "version": "1.2.6", @@ -42,9 +154,9 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "follow-redirects": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" }, "form-data": { "version": "3.0.1", diff --git a/package.json b/package.json index 5b77884..1212de9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-request-action", - "version": "1.7.3", + "version": "1.8.0", "description": "", "main": "src/index.js", "private": false, diff --git a/src/httpClient.js b/src/httpClient.js index 9db35be..e35ee9d 100644 --- a/src/httpClient.js +++ b/src/httpClient.js @@ -5,7 +5,21 @@ const fs = require('fs') const METHOD_GET = 'GET' const METHOD_POST = 'POST' -const request = async({ method, instanceConfig, data, files, auth, actions, preventFailureOnNoResponse, escapeData }) => { +/** + * @param {Object} param0 + * @param {string} param0.method HTTP Method + * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} param0.instanceConfig + * @param {string} param0.data Request Body as string, default {} + * @param {string} param0.files Map of Request Files (name: absolute path) as JSON String, default: {} + * @param {{ username: string; password: string }|undefined} param0.auth Optional HTTP Basic Auth + * @param {*} 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 + * + * @returns {void} + */ +const request = async({ method, instanceConfig, data, files, auth, actions, ignoredCodes, preventFailureOnNoResponse, escapeData }) => { try { if (escapeData) { data = data.replace(/"[^"]*"/g, (match) => { @@ -51,11 +65,13 @@ const request = async({ method, instanceConfig, data, files, auth, actions, prev actions.setOutput('response', JSON.stringify(response.data)) } catch (error) { if (error.toJSON) { - actions.setOutput(JSON.stringify(error.toJSON())); + actions.setOutput('requestError', JSON.stringify(error.toJSON())); } - if (error.response) { - actions.setFailed(JSON.stringify({ code: error.response.code, message: error.response.data })) + if (error.response && ignoredCodes.includes(error.response.status)) { + 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 })) } else if (error.request && !preventFailureOnNoResponse) { actions.setFailed(JSON.stringify({ error: "no response received" })); } else if (error.request && preventFailureOnNoResponse) { @@ -66,6 +82,11 @@ const request = async({ method, instanceConfig, data, files, auth, actions, prev } } +/** + * @param {string} value + * + * @returns {Object} + */ const convertToJSON = (value) => { try { return JSON.parse(value) @@ -74,6 +95,12 @@ const convertToJSON = (value) => { } } +/** + * @param {Object} data + * @param {Object} files + * + * @returns {FormData} + */ const convertToFormData = (data, files) => { formData = new FormData() @@ -88,6 +115,13 @@ const convertToFormData = (data, files) => { return formData } +/** + * @param {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} instanceConfig + * @param {FormData} formData + * @param {*} actions + * + * @returns {{ baseURL: string; timeout: number; headers: { [name: string]: string } }} + */ const updateConfig = async (instanceConfig, formData, actions) => { try { const formHeaders = formData.getHeaders() @@ -109,6 +143,11 @@ const updateConfig = async (instanceConfig, formData, actions) => { } } +/** + * @param {FormData} formData + * + * @returns {Promise} + */ const contentLength = (formData) => new Promise((resolve, reject) => { formData.getLength((err, length) => { if (err) { diff --git a/src/index.js b/src/index.js index a068bb1..e119a8b 100644 --- a/src/index.js +++ b/src/index.js @@ -40,4 +40,11 @@ const method = core.getInput('method') || METHOD_POST; const preventFailureOnNoResponse = core.getInput('preventFailureOnNoResponse') === 'true'; const escapeData = core.getInput('escapeData') === 'true'; -request({ data, method, instanceConfig, auth, preventFailureOnNoResponse, escapeData, files, actions: new GithubActions() }) +const ignoreStatusCodes = core.getInput('ignoreStatusCodes') +let ignoredCodes = [] + +if (typeof ignoreStatusCodes === 'string' && ignoreStatusCodes.length > 0) { + ignoredCodes = ignoreStatusCodes.split(',').map(statusCode => parseInt(statusCode.trim())) +} + +request({ data, method, instanceConfig, auth, preventFailureOnNoResponse, escapeData, files, ignoredCodes, actions: new GithubActions() })