From 31b3a4c342101df6d4eaaaa38fb4a15f32b862de Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sat, 12 Mar 2022 13:47:29 +0000 Subject: [PATCH 1/6] Add UI for common headers/values - Wrap HTML node script in IFFE (isolate module level vars & functions) - Add UI elements for setting headers in http req node edit form - Update built in help - Add tests --- .../nodes/core/network/21-httprequest.html | 210 +++++++++++++++++- .../nodes/core/network/21-httprequest.js | 117 ++++++++-- .../locales/en-US/network/21-httprequest.html | 2 +- .../nodes/core/network/21-httprequest_spec.js | 57 ++++- 4 files changed, 352 insertions(+), 34 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html index bf1f76b7f..31f360992 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html +++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html @@ -104,10 +104,100 @@ +
+ +
+
+
    +
    diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js index bf677b490..6a19d2ed2 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js +++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js @@ -73,7 +73,7 @@ in your Node-RED user directory (${RED.settings.userDir}). var paytobody = false; var redirectList = []; var sendErrorsToCatch = n.senderr; - + node.headers = n.headers || []; var nodeHTTPPersistent = n["persist"]; if (n.tls) { var tlsNode = RED.nodes.getNode(n.tls); @@ -105,6 +105,37 @@ in your Node-RED user directory (${RED.settings.userDir}). timingLog = RED.settings.httpRequestTimingLog; } + /** + * Case insensitive header value update util function + * @param {object} headersObject The opt.headers object to update + * @param {string} name The header name + * @param {string} value The header value to set (if blank, header is removed) + */ + const updateHeader = function(headersObject, name, value ) { + const hn = name.toLowerCase(); + const keys = Object.keys(headersObject); + const matchingKeys = keys.filter(e => e.toLowerCase() == hn) + const updateKey = (k,v) => { + delete headersObject[k]; //delete incase of case change + if(v) { headersObject[name] = v } //re-add with requested name & value + } + if(matchingKeys.length == 0) { + updateKey(name, value) + } else { + matchingKeys.forEach(k => { + updateKey(k, value); + }); + } + } + /** + * @param {Object} headersObject + * @param {string} name + * @return {any} value + */ + const getHeaderValue = (headersObject, name) => { + const asLowercase = name.toLowercase(); + return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)]; + } this.on("input",function(msg,nodeSend,nodeDone) { checkNodeAgentPatch(); //reset redirectList on each request @@ -183,7 +214,6 @@ in your Node-RED user directory (${RED.settings.userDir}). // TODO: add UI option to auto decompress. Setting to false for 1.x compatibility opts.decompress = false; opts.method = method; - opts.headers = {}; opts.retry = 0; opts.responseType = 'buffer'; opts.maxRedirects = 21; @@ -229,34 +259,85 @@ in your Node-RED user directory (${RED.settings.userDir}). ] } - var ctSet = "Content-Type"; // set default camel case - var clSet = "Content-Length"; + let ctSet = "Content-Type"; // set default camel case + let clSet = "Content-Length"; + const normaliseKnownHeader = function (name) { + const _name = name.toLowerCase(); + // only normalise the known headers used later in this + // function. Otherwise leave them alone. + switch (_name) { + case "content-type": + ctSet = name; + name = _name; + break; + case "content-length": + clSet = name; + name = _name; + break; + } + return name; + } + + opts.headers = {}; + //add msg.headers + //NOTE: ui headers will take precidence over msg.headers if (msg.headers) { if (msg.headers.hasOwnProperty('x-node-red-request-node')) { - var headerHash = msg.headers['x-node-red-request-node']; + const headerHash = msg.headers['x-node-red-request-node']; delete msg.headers['x-node-red-request-node']; - var hash = hashSum(msg.headers); + const hash = hashSum(msg.headers); if (hash === headerHash) { delete msg.headers; } } if (msg.headers) { - for (var v in msg.headers) { - if (msg.headers.hasOwnProperty(v)) { - var name = v.toLowerCase(); - if (name !== "content-type" && name !== "content-length") { - // only normalise the known headers used later in this - // function. Otherwise leave them alone. - name = v; - } - else if (name === 'content-type') { ctSet = v; } - else { clSet = v; } - opts.headers[name] = msg.headers[v]; - } + for (let hn in msg.headers) { + const name = normaliseKnownHeader(hn); + updateHeader(opts.headers, name, msg.headers[hn]); } } } + //add/remove/update headers from UI. + if (node.headers.length) { + for (let index = 0; index < node.headers.length; index++) { + const header = node.headers[index]; + let headerName, headerValue; + if (header.keyType === "other") { + headerName = header.keyValue + } else if (header.keyType === "msg") { + RED.util.evaluateNodeProperty(header.keyValue, header.keyType, node, msg, (err, value) => { + if (err) { + //ignore header + } else { + headerName = value; + } + }); + } else { + headerName = header.keyType + } + if (!headerName) { + continue; //skip if header name is empyy + } + if (header.valueType === "other") { + headerValue = header.valueValue + } else if (header.valueType === "msg") { + RED.util.evaluateNodeProperty(header.valueValue, header.valueType, node, msg, (err, value) => { + if (err) { + //ignore header + } else { + headerValue = value; + } + }); + } else { + headerValue = header.valueType + } + const hn = normaliseKnownHeader(headerName); + updateHeader(opts.headers, hn, headerValue); + } + } + + if (msg.hasOwnProperty('followRedirects')) { opts.followRedirect = !!msg.followRedirects; } diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/locales/en-US/network/21-httprequest.html index 68d9d8f1b..d2cba3af7 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/network/21-httprequest.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/network/21-httprequest.html @@ -25,7 +25,7 @@
    If not configured in the node, this optional property sets the HTTP method of the request. Must be one of GET, PUT, POST, PATCH or DELETE.
    headers object
    -
    Sets the HTTP headers of the request.
    +
    Sets the HTTP headers of the request. NOTE: Any headers set in the UI will overwrite any matching headers in msg.headers
    cookies object
    If set, can be used to send cookies with the request.
    payload
    diff --git a/test/nodes/core/network/21-httprequest_spec.js b/test/nodes/core/network/21-httprequest_spec.js index 07bed5a01..688776289 100644 --- a/test/nodes/core/network/21-httprequest_spec.js +++ b/test/nodes/core/network/21-httprequest_spec.js @@ -1500,6 +1500,50 @@ describe('HTTP Request Node', function() { n1.receive({payload:{foo:"bar"}, headers: { 'content-type': 'text/plain', "x-node-red-request-node":"INVALID_SUM"}}); }); }); + + it('should use ui headers', function (done) { + const flow = [ + { + id: "n1", type: "http request", wires: [["n2"]], method: "GET", ret: "obj", url: getTestURL('/rawHeaders'), + "headers": [ + { "keyType": "Accept", "keyValue": "", "valueType": "application/json", "valueValue": "" },//set "Accept" to "application/json" + { "keyType": "Content-Type", "keyValue": "", "valueType": "application/json", "valueValue": "" },//overwrite msg.headers['content-type'] with UI header 'Content-Type'] + { "keyType": "msg", "keyValue": "dynamicHeaderName", "valueType": "msg", "valueValue": "dynamicHeaderValue" }, //dynamic msg.dynamicHeaderName/msg.dynamicHeaderValue + { "keyType": "other", "keyValue": "static-header-name", "valueType": "other", "valueValue": "static-header-value" }, //static "other" header and value + { "keyType": "Location", "keyValue": "", "valueType": "other", "valueValue": "" }, //delete "Location" header (initially set in msg.headers['location'] by passing empty string for value + ] + }, + { id: "n2", type: "helper" }]; + helper.load(httpRequestNode, flow, function () { + const n1 = helper.getNode("n1"); + const n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property('statusCode',200); + msg.payload.should.have.property('headers'); + //msg.headers['Accept'] should be set by Flow UI + msg.payload.headers.should.have.property('Accept').which.startWith('application/json'); + //msg.headers['content-type'] should be updated to 'Content-Type' by the value set in the Flow UI + msg.payload.headers.should.have.property('Content-Type').which.startWith('application/json'); + //msg.dynamicHeaderName should be present in headers with the value of msg.dynamicHeaderValue + msg.payload.headers.should.have.property('dyn-header-name').which.startWith('dyn-header-value'); + //static (custom) header set in Flow UI should be present + msg.payload.headers.should.have.property('static-header-name').which.startWith('static-header-value'); + //msg.headers['location'] should be deleted because Flow UI "Location" header has a blank value + //ensures headers with matching characters but different case are eliminated + msg.payload.headers.should.not.have.property('location'); + msg.payload.headers.should.not.have.property('Location'); + done(); + } catch (err) { + done(err); + } + }); + // Pass in a headers property with a "content-type" & "location" header + // Pass in dynamicHeaderName & dynamicHeaderValue properties to set the Flow UI `msg.xxx` header entries + n1.receive({ payload: { foo: "bar" }, dynamicHeaderName: "dyn-header-name", dynamicHeaderValue: "dyn-header-value", headers: { 'content-type': 'text/plain', 'location': 'london' } }); + }); + }); + }); describe('protocol', function() { @@ -1976,8 +2020,14 @@ describe('HTTP Request Node', function() { describe('file-upload', function() { it('should upload a file', function(done) { - var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'POST',ret:'obj',url:getTestURL('/file-upload')}, - {id:"n2", type:"helper"}]; + const flow = [ + { + id: 'n1', type: 'http request', wires: [['n2']], method: 'POST', ret: 'obj', url: getTestURL('/file-upload'), headers: [ + { "keyType": "Content-Type", "keyValue": "", "valueType": "multipart/form-data", "valueValue": "" } + ] + }, + { id: "n2", type: "helper" } + ]; helper.load(httpRequestNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -1995,9 +2045,6 @@ describe('HTTP Request Node', function() { } }); n1.receive({ - headers: { - 'content-type':'multipart/form-data' - }, payload: { file: { value: Buffer.from("Hello World"), From 600713264087ff9336533f7ef8e6457f5aeca67a Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sat, 12 Mar 2022 14:59:15 +0000 Subject: [PATCH 2/6] rearange UI (name to bottom) --- .../nodes/core/network/21-httprequest.html | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html index 31f360992..ee393627f 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html +++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html @@ -100,17 +100,20 @@ -
    - - -
    + + +
      - + +
      + + +