@inteli.city/node-red-contrib-http-plus 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/httpin+.html +66 -12
- package/httpin+.js +78 -1
- package/package.json +1 -1
package/httpin+.html
CHANGED
|
@@ -80,6 +80,17 @@
|
|
|
80
80
|
<p><b>Cognito JWT:</b> validates a Bearer token against a JWKS endpoint. Enable <b>"Expose user to flow"</b> in the config node to populate <code>msg.user</code> with mapped JWT claims. When disabled, no JWT data enters the flow.</p>
|
|
81
81
|
<p>After successful authentication, <code>Authorization</code> headers and <code>?token=</code> query parameters are always removed before the message is dispatched.</p>
|
|
82
82
|
|
|
83
|
+
<h3>Backend Cache</h3>
|
|
84
|
+
<p>Enable <b>Enable backend cache</b> to cache GET responses on the server. Repeated identical requests skip the flow entirely and return the cached response.</p>
|
|
85
|
+
<ul>
|
|
86
|
+
<li>Only active for GET requests with no authentication.</li>
|
|
87
|
+
<li>Cache key is derived from the URL path and query parameters.</li>
|
|
88
|
+
<li>Responses are stored in <code><userDir>/cache/http+/</code>.</li>
|
|
89
|
+
<li>Expired entries are ignored lazily — no active cleanup is needed.</li>
|
|
90
|
+
<li>The response header <code>X-Cache: HIT</code> or <code>MISS</code> indicates cache status.</li>
|
|
91
|
+
</ul>
|
|
92
|
+
<p>Not suitable for dynamic or user-specific responses.</p>
|
|
93
|
+
|
|
83
94
|
<h3>Details</h3>
|
|
84
95
|
<p>The URL field supports Express route patterns, including named parameters such as <code>/users/:id</code>.</p>
|
|
85
96
|
<p>For more examples see the package <a href="https://github.com/inteli-city/node-red-contrib-http-plus">README</a>.</p>
|
|
@@ -104,14 +115,15 @@
|
|
|
104
115
|
|
|
105
116
|
<h3>Details</h3>
|
|
106
117
|
<p>This node is the standard response pair for <b>http.in+</b>.</p>
|
|
107
|
-
<p>It is fully compatible with the built-in Node-RED <i>http response</i> node, with
|
|
118
|
+
<p>It is fully compatible with the built-in Node-RED <i>http response</i> node, with two additions: browser cache control and streaming support.</p>
|
|
108
119
|
<p>The <code>msg.res</code> object from the originating <b>http.in+</b> must reach this node unchanged.</p>
|
|
109
120
|
|
|
110
|
-
<h3>
|
|
111
|
-
<p>
|
|
121
|
+
<h3>Browser Cache</h3>
|
|
122
|
+
<p>Enable <b>Enable browser cache</b> to automatically set <code>Cache-Control: public, max-age=<seconds></code> on every response.</p>
|
|
112
123
|
<ul>
|
|
113
|
-
<li>
|
|
114
|
-
<li>
|
|
124
|
+
<li>Safe for static assets (images, scripts, stylesheets).</li>
|
|
125
|
+
<li>Avoid for dynamic or user-specific responses.</li>
|
|
126
|
+
<li>If <code>msg.headers['cache-control']</code> is already set in the flow, it takes precedence and the node-level setting is ignored.</li>
|
|
115
127
|
</ul>
|
|
116
128
|
|
|
117
129
|
<h3>Streaming</h3>
|
|
@@ -210,25 +222,51 @@
|
|
|
210
222
|
<b>Zod docs</b><br>
|
|
211
223
|
<a href="https://zod.dev/api" target="_blank">https://zod.dev/api</a>
|
|
212
224
|
</div>
|
|
225
|
+
<div class="form-row">
|
|
226
|
+
<input type="checkbox" id="node-input-enableCache" style="display:inline-block; width:auto; vertical-align:top;">
|
|
227
|
+
<label for="node-input-enableCache" style="width:auto;"> Enable backend cache</label>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="form-row httpin-cache-duration-row">
|
|
230
|
+
<label for="node-input-cacheDuration"><i class="fa fa-clock-o"></i> Cache duration (s)</label>
|
|
231
|
+
<input type="number" id="node-input-cacheDuration" style="width:100px;" min="1" placeholder="300">
|
|
232
|
+
</div>
|
|
233
|
+
<div class="httpin-cache-duration-row" style="font-size:12px; color:#666; margin: -4px 0 8px 110px; line-height:1.5;">
|
|
234
|
+
Caches GET responses server-side. Repeated identical requests skip the flow entirely.<br>
|
|
235
|
+
Only active for unauthenticated GET requests.<br>
|
|
236
|
+
Not suitable for dynamic or user-specific responses.
|
|
237
|
+
</div>
|
|
213
238
|
<div id="node-input-tip" class="form-tips"><span data-i18n="httpin.tip.in"></span><code><span id="node-input-path"></span></code>.</div>
|
|
214
239
|
</script>
|
|
215
240
|
|
|
216
241
|
<script type="text/html" data-template-name="http.out+">
|
|
217
242
|
<div class="form-row">
|
|
218
|
-
<label for="node-input-name"><i class="fa fa-tag"></i>
|
|
219
|
-
<input type="text" id="node-input-name"
|
|
243
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
244
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
220
245
|
</div>
|
|
221
246
|
<div class="form-row">
|
|
222
|
-
<label for="node-input-statusCode"><i class="fa fa-long-arrow-left"></i>
|
|
247
|
+
<label for="node-input-statusCode"><i class="fa fa-long-arrow-left"></i> Status code</label>
|
|
223
248
|
<input type="text" id="node-input-statusCode" placeholder="msg.statusCode">
|
|
224
249
|
</div>
|
|
250
|
+
<div class="form-row">
|
|
251
|
+
<input type="checkbox" id="node-input-enableCache" style="display:inline-block; width:auto; vertical-align:top;">
|
|
252
|
+
<label for="node-input-enableCache" style="width:auto;"> Enable browser cache</label>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="form-row http-out-cache-duration-row">
|
|
255
|
+
<label for="node-input-cacheDuration"><i class="fa fa-clock-o"></i> Cache duration (s)</label>
|
|
256
|
+
<input type="number" id="node-input-cacheDuration" style="width:100px;" min="1" placeholder="3600">
|
|
257
|
+
</div>
|
|
258
|
+
<div class="http-out-cache-duration-row" style="font-size:12px; color:#666; margin: -4px 0 8px 110px; line-height:1.5;">
|
|
259
|
+
Sets <code>Cache-Control: public, max-age=N</code> on the response.<br>
|
|
260
|
+
Safe for static assets. Avoid for dynamic or user-specific responses.<br>
|
|
261
|
+
Overridden by <code>msg.headers['cache-control']</code> if set in the flow.
|
|
262
|
+
</div>
|
|
225
263
|
<div class="form-row" style="margin-bottom:0;">
|
|
226
|
-
<label><i class="fa fa-list"></i>
|
|
264
|
+
<label><i class="fa fa-list"></i> Headers</label>
|
|
227
265
|
</div>
|
|
228
266
|
<div class="form-row node-input-headers-container-row">
|
|
229
267
|
<ol id="node-input-headers-container"></ol>
|
|
230
268
|
</div>
|
|
231
|
-
<div class="form-tips"
|
|
269
|
+
<div class="form-tips">The messages sent to this node <b>must</b> originate from an <i>http input</i> node</div>
|
|
232
270
|
</script>
|
|
233
271
|
|
|
234
272
|
<script type="text/javascript">
|
|
@@ -248,7 +286,9 @@
|
|
|
248
286
|
uploadDir: {value: ''},
|
|
249
287
|
authConfig: {type:"http.auth.config+", required:false},
|
|
250
288
|
enableZod: {value: false},
|
|
251
|
-
zodSchema: {value: ""}
|
|
289
|
+
zodSchema: {value: ""},
|
|
290
|
+
enableCache: {value: false},
|
|
291
|
+
cacheDuration: {value: 300}
|
|
252
292
|
},
|
|
253
293
|
inputs:0,
|
|
254
294
|
outputs:1,
|
|
@@ -316,6 +356,12 @@
|
|
|
316
356
|
$("#node-input-enableZod").on("change", toggleZodSchema);
|
|
317
357
|
toggleZodSchema();
|
|
318
358
|
|
|
359
|
+
function toggleCacheDuration() {
|
|
360
|
+
$(".httpin-cache-duration-row").toggle($("#node-input-enableCache").is(":checked"));
|
|
361
|
+
}
|
|
362
|
+
$("#node-input-enableCache").on("change", toggleCacheDuration);
|
|
363
|
+
toggleCacheDuration();
|
|
364
|
+
|
|
319
365
|
var httpRoot = (RED.settings.httpNodeRoot || "").replace(/\/$/, "");
|
|
320
366
|
var docsUrl = window.location.origin + httpRoot + "/docs";
|
|
321
367
|
var specUrl = window.location.origin + httpRoot + "/openapi.json";
|
|
@@ -367,7 +413,9 @@
|
|
|
367
413
|
value:"",
|
|
368
414
|
label: RED._("node-red:httpin.label.status"),
|
|
369
415
|
validate: RED.validators.number(true)},
|
|
370
|
-
headers: {value:{}}
|
|
416
|
+
headers: {value:{}},
|
|
417
|
+
enableCache: {value: false},
|
|
418
|
+
cacheDuration: {value: 3600}
|
|
371
419
|
},
|
|
372
420
|
inputs:1,
|
|
373
421
|
outputs:0,
|
|
@@ -380,6 +428,12 @@
|
|
|
380
428
|
return this.name?"node_label_italic":"";
|
|
381
429
|
},
|
|
382
430
|
oneditprepare: function() {
|
|
431
|
+
function toggleCacheDuration() {
|
|
432
|
+
$(".http-out-cache-duration-row").toggle($("#node-input-enableCache").is(":checked"));
|
|
433
|
+
}
|
|
434
|
+
$("#node-input-enableCache").on("change", toggleCacheDuration);
|
|
435
|
+
toggleCacheDuration();
|
|
436
|
+
|
|
383
437
|
var headerList = $("#node-input-headers-container").css('min-height','150px').css('min-width','450px').editableList({
|
|
384
438
|
addItem: function(container,i,header) {
|
|
385
439
|
var row = $('<div/>').css({
|
package/httpin+.js
CHANGED
|
@@ -38,7 +38,10 @@ module.exports = function(RED) {
|
|
|
38
38
|
var jwt = require('jsonwebtoken');
|
|
39
39
|
var jwksRsa = require('jwks-rsa');
|
|
40
40
|
var { z } = require('zod');
|
|
41
|
-
var os
|
|
41
|
+
var os = require('os');
|
|
42
|
+
var path = require('path');
|
|
43
|
+
var fs = require('fs');
|
|
44
|
+
var crypto = require('crypto');
|
|
42
45
|
|
|
43
46
|
function rawBodyParser(req, res, next) {
|
|
44
47
|
if (req.skipRawBodyParser) { next(); } // don't parse this if told to skip
|
|
@@ -187,6 +190,29 @@ module.exports = function(RED) {
|
|
|
187
190
|
RED.httpNode.options("*",corsHandler);
|
|
188
191
|
}
|
|
189
192
|
|
|
193
|
+
// ── Backend cache utilities ────────────────────────────────────────────
|
|
194
|
+
function computeCacheKey(req) {
|
|
195
|
+
var query = req.query || {};
|
|
196
|
+
var sorted = Object.keys(query).sort().map(function(k) {
|
|
197
|
+
return encodeURIComponent(k) + '=' + encodeURIComponent(String(query[k]));
|
|
198
|
+
}).join('&');
|
|
199
|
+
return crypto.createHash('sha256').update(req.path + '?' + sorted).digest('hex');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readCacheEntry(dir, key) {
|
|
203
|
+
try {
|
|
204
|
+
var data = JSON.parse(fs.readFileSync(path.join(dir, key + '.json'), 'utf8'));
|
|
205
|
+
return Date.now() < data.expires ? data : null;
|
|
206
|
+
} catch(e) { return null; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function writeCacheEntry(dir, key, entry) {
|
|
210
|
+
try {
|
|
211
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
212
|
+
fs.writeFileSync(path.join(dir, key + '.json'), JSON.stringify(entry), 'utf8');
|
|
213
|
+
} catch(e) {}
|
|
214
|
+
}
|
|
215
|
+
|
|
190
216
|
function HTTPIn(n) {
|
|
191
217
|
RED.nodes.createNode(this,n);
|
|
192
218
|
|
|
@@ -255,6 +281,9 @@ module.exports = function(RED) {
|
|
|
255
281
|
this.method = n.method;
|
|
256
282
|
this.swaggerDoc = n.swaggerDoc;
|
|
257
283
|
this.enableUpload = n.enableUpload;
|
|
284
|
+
this.enableCache = n.enableCache === true;
|
|
285
|
+
this.cacheDuration = parseInt(n.cacheDuration) || 300;
|
|
286
|
+
this.cacheDir = path.join(RED.settings.userDir || process.cwd(), 'cache', 'http+');
|
|
258
287
|
this.uploadStorage = n.uploadStorage || 'memory';
|
|
259
288
|
this.maxFileSize = n.maxFileSize || 5;
|
|
260
289
|
this.uploadDir = n.uploadDir || '';
|
|
@@ -267,6 +296,24 @@ module.exports = function(RED) {
|
|
|
267
296
|
};
|
|
268
297
|
|
|
269
298
|
this.callback = function(req,res) {
|
|
299
|
+
// ── Backend cache hit check ────────────────────────────────
|
|
300
|
+
var isAuthFree = !authConfig || authConfig.authType === 'none';
|
|
301
|
+
if (node.enableCache && node.method === 'get' && isAuthFree) {
|
|
302
|
+
var cKey = computeCacheKey(req);
|
|
303
|
+
var hit = readCacheEntry(node.cacheDir, cKey);
|
|
304
|
+
if (hit) {
|
|
305
|
+
res.set('X-Cache', 'HIT');
|
|
306
|
+
if (hit.contentType) { res.set('Content-Type', hit.contentType); }
|
|
307
|
+
var hitBody = hit.bodyEncoding === 'base64'
|
|
308
|
+
? Buffer.from(hit.body, 'base64')
|
|
309
|
+
: hit.body;
|
|
310
|
+
return res.status(hit.statusCode).send(hitBody);
|
|
311
|
+
}
|
|
312
|
+
req._httpPlusCacheKey = cKey;
|
|
313
|
+
req._httpPlusCacheTtl = node.cacheDuration;
|
|
314
|
+
req._httpPlusCacheDir = node.cacheDir;
|
|
315
|
+
}
|
|
316
|
+
|
|
270
317
|
// Dispatch msg downstream after successful auth
|
|
271
318
|
function dispatchMsg(user) {
|
|
272
319
|
var msgid = RED.util.generateId();
|
|
@@ -531,6 +578,8 @@ module.exports = function(RED) {
|
|
|
531
578
|
var node = this;
|
|
532
579
|
this.headers = n.headers||{};
|
|
533
580
|
this.statusCode = parseInt(n.statusCode);
|
|
581
|
+
this.enableCache = n.enableCache === true;
|
|
582
|
+
this.cacheDuration = parseInt(n.cacheDuration) || 3600;
|
|
534
583
|
this.on("input",function(msg,_send,done) {
|
|
535
584
|
if (msg.res) {
|
|
536
585
|
var headers = RED.util.cloneMessage(node.headers);
|
|
@@ -551,6 +600,13 @@ module.exports = function(RED) {
|
|
|
551
600
|
}
|
|
552
601
|
}
|
|
553
602
|
}
|
|
603
|
+
// ── Browser cache ──────────────────────────────────────────
|
|
604
|
+
if (node.enableCache && !headers.hasOwnProperty('cache-control')) {
|
|
605
|
+
headers['cache-control'] = 'public, max-age=' + node.cacheDuration;
|
|
606
|
+
}
|
|
607
|
+
// ── Backend cache write coordination ───────────────────────
|
|
608
|
+
var rawReq = msg.res._res && msg.res._res.req;
|
|
609
|
+
var httpPlusCacheKey = rawReq && rawReq._httpPlusCacheKey;
|
|
554
610
|
// ── Streaming mode ─────────────────────────────────────────
|
|
555
611
|
var stream = msg.stream || (msg.payload && typeof msg.payload.pipe === 'function' && !Buffer.isBuffer(msg.payload) ? msg.payload : null);
|
|
556
612
|
if (stream) {
|
|
@@ -572,6 +628,9 @@ module.exports = function(RED) {
|
|
|
572
628
|
return;
|
|
573
629
|
}
|
|
574
630
|
|
|
631
|
+
if (httpPlusCacheKey) {
|
|
632
|
+
headers['x-cache'] = 'MISS';
|
|
633
|
+
}
|
|
575
634
|
if (Object.keys(headers).length > 0) {
|
|
576
635
|
msg.res._res.set(headers);
|
|
577
636
|
}
|
|
@@ -596,6 +655,14 @@ module.exports = function(RED) {
|
|
|
596
655
|
var statusCode = node.statusCode || parseInt(msg.statusCode) || 200;
|
|
597
656
|
if (typeof msg.payload == "object" && !Buffer.isBuffer(msg.payload)) {
|
|
598
657
|
msg.res._res.status(statusCode).jsonp(msg.payload);
|
|
658
|
+
if (httpPlusCacheKey) {
|
|
659
|
+
writeCacheEntry(rawReq._httpPlusCacheDir, httpPlusCacheKey, {
|
|
660
|
+
expires: Date.now() + rawReq._httpPlusCacheTtl * 1000,
|
|
661
|
+
statusCode: statusCode,
|
|
662
|
+
contentType: 'application/json',
|
|
663
|
+
body: JSON.stringify(msg.payload)
|
|
664
|
+
});
|
|
665
|
+
}
|
|
599
666
|
} else {
|
|
600
667
|
if (msg.res._res.get('content-length') == null) {
|
|
601
668
|
var len;
|
|
@@ -615,6 +682,16 @@ module.exports = function(RED) {
|
|
|
615
682
|
msg.payload = ""+msg.payload;
|
|
616
683
|
}
|
|
617
684
|
msg.res._res.status(statusCode).send(msg.payload);
|
|
685
|
+
if (httpPlusCacheKey) {
|
|
686
|
+
var isBuffer = Buffer.isBuffer(msg.payload);
|
|
687
|
+
writeCacheEntry(rawReq._httpPlusCacheDir, httpPlusCacheKey, {
|
|
688
|
+
expires: Date.now() + rawReq._httpPlusCacheTtl * 1000,
|
|
689
|
+
statusCode: statusCode,
|
|
690
|
+
contentType: msg.res._res.getHeader('content-type') || '',
|
|
691
|
+
body: isBuffer ? msg.payload.toString('base64') : String(msg.payload == null ? '' : msg.payload),
|
|
692
|
+
bodyEncoding: isBuffer ? 'base64' : 'text'
|
|
693
|
+
});
|
|
694
|
+
}
|
|
618
695
|
}
|
|
619
696
|
} else {
|
|
620
697
|
node.warn(RED._("httpin.errors.no-response"));
|