@inteli.city/node-red-contrib-http-plus 1.0.1 → 1.0.2
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/README.md +136 -3
- package/httpin+.html +93 -29
- package/httpin+.js +59 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @inteli.city/node-red-contrib-http+
|
|
2
2
|
|
|
3
|
-
Enhanced HTTP nodes for Node-RED with built-in authentication
|
|
3
|
+
Enhanced HTTP nodes for Node-RED with built-in authentication, request validation, and caching.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -21,6 +21,8 @@ These nodes are designed to be compatible with the standard Node-RED HTTP flow w
|
|
|
21
21
|
| File upload handling | Basic | Structured (`msg.files`) |
|
|
22
22
|
| Data normalization | Manual | Built-in via Zod transforms |
|
|
23
23
|
| Streaming responses | Limited/manual | First-class support |
|
|
24
|
+
| Backend caching | Not available | Built-in (`http.in+`) |
|
|
25
|
+
| Browser cache control | Manual headers | Built-in (`http.out+`) |
|
|
24
26
|
|
|
25
27
|
---
|
|
26
28
|
|
|
@@ -57,6 +59,7 @@ Use standard nodes when:
|
|
|
57
59
|
- [Nodes](#nodes)
|
|
58
60
|
- [Install](#install)
|
|
59
61
|
- [Core Concepts](#core-concepts)
|
|
62
|
+
- [Caching](#caching)
|
|
60
63
|
- [Authentication](#authentication)
|
|
61
64
|
- [Validation with Zod](#validation-with-zod)
|
|
62
65
|
- [Swagger / OpenAPI](#swagger--openapi)
|
|
@@ -71,9 +74,9 @@ Use standard nodes when:
|
|
|
71
74
|
|
|
72
75
|
| Node | Role |
|
|
73
76
|
|------|------|
|
|
74
|
-
| `http.in+` | Receives HTTP requests. Runs auth
|
|
77
|
+
| `http.in+` | Receives HTTP requests. Runs auth, Zod validation, and optional backend caching before sending `msg` downstream. |
|
|
75
78
|
| `http.request+` | Makes outgoing HTTP requests. Drop-in replacement for the standard `http request` node. |
|
|
76
|
-
| `http.out+` | Sends the HTTP response back to the caller.
|
|
79
|
+
| `http.out+` | Sends the HTTP response back to the caller. Optionally controls browser caching via `Cache-Control`. |
|
|
77
80
|
|
|
78
81
|
## Install
|
|
79
82
|
|
|
@@ -110,6 +113,118 @@ inject ──→ http.request+ ──→ debug
|
|
|
110
113
|
|
|
111
114
|
---
|
|
112
115
|
|
|
116
|
+
## Caching
|
|
117
|
+
|
|
118
|
+
http+ supports two distinct caching mechanisms that operate at different layers and solve different problems.
|
|
119
|
+
|
|
120
|
+
| Feature | Backend cache (`http.in+`) | Browser cache (`http.out+`) |
|
|
121
|
+
|---------|----------------------------|-----------------------------|
|
|
122
|
+
| Layer | Server | Client (browser) |
|
|
123
|
+
| When applied | Before flow execution | After response is sent |
|
|
124
|
+
| Effect | Skips the flow entirely on a hit | Browser may reuse the response |
|
|
125
|
+
| Default | Disabled | Disabled |
|
|
126
|
+
| Risk | Serving stale or incorrect data | Stale data in the browser |
|
|
127
|
+
|
|
128
|
+
They can be used independently or combined.
|
|
129
|
+
|
|
130
|
+
### How they work
|
|
131
|
+
|
|
132
|
+
**Backend cache** runs at the entry point of the flow. If a valid cached response exists for the incoming request, it is returned immediately — no downstream node runs.
|
|
133
|
+
|
|
134
|
+
**Browser cache** runs at the response level. It tells the client how long to keep the response locally. The flow still runs on every server request; the browser decides whether to send the request at all.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### Backend cache (http.in+)
|
|
139
|
+
|
|
140
|
+
Caches full HTTP responses on the server. Repeated identical GET requests are served from cache without executing any downstream nodes.
|
|
141
|
+
|
|
142
|
+
Active only for `GET` requests. The **Cache** field controls the scope:
|
|
143
|
+
|
|
144
|
+
| Mode | Who shares the cache | Auth required |
|
|
145
|
+
|------|----------------------|---------------|
|
|
146
|
+
| Disabled | — | — |
|
|
147
|
+
| Public | All requests with the same URL + query | No |
|
|
148
|
+
| Per-user | Each authenticated user has their own entry | Yes |
|
|
149
|
+
| By claim | Users sharing the same value for a chosen identity field | Yes |
|
|
150
|
+
|
|
151
|
+
**Cache key** — derived from URL path + sorted query parameters + identity (for per-user and by-claim modes). Headers, cookies, and request body are not part of the key.
|
|
152
|
+
|
|
153
|
+
**Storage** — responses are stored on disk:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
<userDir>/cache/http+/
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The directory is created automatically if it does not exist.
|
|
160
|
+
|
|
161
|
+
**TTL** — controlled by the "Cache duration (s)" field. Expired entries are ignored and replaced on the next request.
|
|
162
|
+
|
|
163
|
+
**Observability** — every cached response includes:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
X-Cache: HIT → served from cache; flow was skipped
|
|
167
|
+
X-Cache: MISS → cache was empty or expired; flow ran
|
|
168
|
+
X-Cache-Scope: public | user | claim:<field>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Examples:**
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
GET /products → Public cache (shared across all callers)
|
|
175
|
+
GET /profile → Per-user cache (isolated per authenticated user)
|
|
176
|
+
GET /dashboard → By claim: tenantId (shared per tenant, isolated between tenants)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### Browser cache (http.out+)
|
|
182
|
+
|
|
183
|
+
Instructs the browser to store the response locally and reuse it for subsequent requests. The server still handles every request that reaches it — the browser controls whether a request is sent at all.
|
|
184
|
+
|
|
185
|
+
**What it does:**
|
|
186
|
+
|
|
187
|
+
When enabled, `http.out+` adds the following header to every response:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
Cache-Control: public, max-age=<duration>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Override rule:** if `msg.headers['cache-control']` is set anywhere in the flow, it takes precedence over the node setting. This allows per-request control without changing node configuration.
|
|
194
|
+
|
|
195
|
+
**Example:**
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
[http.in+ GET /logo.png] ──→ [read file] ──→ [http.out+ browser cache: 86400 s]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The browser caches the image for 24 hours. On subsequent page loads, no network request is made.
|
|
202
|
+
|
|
203
|
+
#### ⚠️ When NOT to use browser cache
|
|
204
|
+
|
|
205
|
+
- Dynamic responses that change between requests
|
|
206
|
+
- User-specific or session-specific data
|
|
207
|
+
- Any endpoint where a stale cached response would cause incorrect behavior
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### Using both together
|
|
212
|
+
|
|
213
|
+
Backend cache and browser cache can be combined on the same endpoint:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
[http.in+ GET /summary cache: 60 s] ──→ [compute] ──→ [http.out+ browser cache: 60 s]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- The server caches the response for 60 seconds — the flow is skipped on repeated hits.
|
|
220
|
+
- The browser also caches it for 60 seconds — the request may not leave the client at all.
|
|
221
|
+
|
|
222
|
+
Align both TTLs to avoid a situation where the browser caches a response longer than the server would serve the same stale version.
|
|
223
|
+
|
|
224
|
+
For highly dynamic data, leave both disabled.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
113
228
|
## Authentication
|
|
114
229
|
|
|
115
230
|
Configure authentication by creating an **`http.auth.config+`** config node and attaching it to `http.in+`.
|
|
@@ -489,6 +604,24 @@ Sends the HTTP response back to the original caller. Must be connected to the sa
|
|
|
489
604
|
|
|
490
605
|
If `msg.payload` is an object, the response is sent as JSON automatically.
|
|
491
606
|
|
|
607
|
+
### Browser caching
|
|
608
|
+
|
|
609
|
+
`http.out+` can automatically set browser cache headers without requiring a function node.
|
|
610
|
+
|
|
611
|
+
Enable **Enable browser cache** in the node editor and set **Cache duration (s)**. The node will add:
|
|
612
|
+
|
|
613
|
+
```
|
|
614
|
+
Cache-Control: public, max-age=<duration>
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
This does not affect server-side execution — the flow always runs when a request reaches the server. The browser uses the header to decide whether to skip the request entirely on subsequent calls.
|
|
618
|
+
|
|
619
|
+
If `msg.headers['cache-control']` is set anywhere in the flow, it overrides the node setting. This allows per-request cache control without changing node configuration.
|
|
620
|
+
|
|
621
|
+
Use for static or infrequently-changing responses (images, scripts, reference data). Avoid for user-specific or time-sensitive responses.
|
|
622
|
+
|
|
623
|
+
For a full explanation including TTL guidance, safety rules, and combining with backend cache, see the [Caching](#caching) section.
|
|
624
|
+
|
|
492
625
|
### Streaming responses
|
|
493
626
|
|
|
494
627
|
You can stream a response instead of sending a buffer. Useful for large files or when proxying a stream from another source.
|
package/httpin+.html
CHANGED
|
@@ -80,16 +80,20 @@
|
|
|
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>
|
|
84
|
-
<p>
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
<
|
|
89
|
-
<
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
<h3>Cache modes</h3>
|
|
84
|
+
<p>The <b>Cache</b> selector controls how GET responses are cached on the server. Cached responses skip the flow entirely.</p>
|
|
85
|
+
<dl>
|
|
86
|
+
<dt>Disabled</dt>
|
|
87
|
+
<dd>No caching. Every request executes the full flow.</dd>
|
|
88
|
+
<dt>Public</dt>
|
|
89
|
+
<dd>One cached response is shared for all requests with the same URL and query parameters. Authentication is ignored — do not use for endpoints that return user-specific data.</dd>
|
|
90
|
+
<dt>Per-user</dt>
|
|
91
|
+
<dd>Each authenticated user has their own cache entry. Requires an auth config node. Responses are never shared between users.</dd>
|
|
92
|
+
<dt>By claim</dt>
|
|
93
|
+
<dd>Cache entries are grouped by the value of a specific identity field (e.g. <code>tenantId</code>, <code>role</code>). Users sharing the same value receive the same cached response.</dd>
|
|
94
|
+
</dl>
|
|
95
|
+
<p>Cache files are stored in <code><userDir>/cache/http+/</code>. Expired entries are evicted lazily.</p>
|
|
96
|
+
<p>Response headers: <code>X-Cache: HIT | MISS</code> and <code>X-Cache-Scope: public | user | claim:<field></code>.</p>
|
|
93
97
|
|
|
94
98
|
<h3>Details</h3>
|
|
95
99
|
<p>The URL field supports Express route patterns, including named parameters such as <code>/users/:id</code>.</p>
|
|
@@ -160,6 +164,30 @@
|
|
|
160
164
|
<label for="node-input-swaggerDoc"><i class="fa fa-file-text-o"></i> <span data-i18n="httpin.label.doc"></span></label>
|
|
161
165
|
<input type="text" id="node-input-swaggerDoc">
|
|
162
166
|
</div>
|
|
167
|
+
<div class="form-row" style="margin-top:10px; margin-bottom:4px; border-top:1px solid #e6e6e6; padding-top:8px;">
|
|
168
|
+
<span style="font-size:11px; color:#bbb; text-transform:uppercase; letter-spacing:0.05em;">Execution</span>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="form-row">
|
|
171
|
+
<label for="node-input-cacheMode"><i class="fa fa-database"></i> Cache</label>
|
|
172
|
+
<select id="node-input-cacheMode" style="width:70%;">
|
|
173
|
+
<option value="disabled">Disabled</option>
|
|
174
|
+
<option value="public">Public</option>
|
|
175
|
+
<option value="per-user">Per-user</option>
|
|
176
|
+
<option value="by-claim">By claim</option>
|
|
177
|
+
</select>
|
|
178
|
+
</div>
|
|
179
|
+
<div id="node-input-cache-mode-help" style="font-size:12px; color:#666; margin: -4px 0 8px 110px; line-height:1.5;"></div>
|
|
180
|
+
<div class="form-row httpin-cache-claim-row">
|
|
181
|
+
<label for="node-input-cacheClaim"><i class="fa fa-key"></i> Claim</label>
|
|
182
|
+
<input type="text" id="node-input-cacheClaim" style="width:70%;" placeholder="e.g. tenantId, role">
|
|
183
|
+
</div>
|
|
184
|
+
<div class="httpin-cache-claim-help-row" style="font-size:12px; color:#666; margin: -4px 0 8px 110px; line-height:1.5;">
|
|
185
|
+
Choose a stable identity field that determines how responses can be shared.
|
|
186
|
+
</div>
|
|
187
|
+
<div class="form-row httpin-cache-duration-row">
|
|
188
|
+
<label for="node-input-cacheDuration"><i class="fa fa-clock-o"></i> Cache duration (s)</label>
|
|
189
|
+
<input type="number" id="node-input-cacheDuration" style="width:100px;" min="1" placeholder="300">
|
|
190
|
+
</div>
|
|
163
191
|
<div class="form-row upload-section">
|
|
164
192
|
<input type="checkbox" id="node-input-enableUpload" style="display:inline-block; width:auto; vertical-align:top;">
|
|
165
193
|
<label for="node-input-enableUpload" style="width:auto;"><i class="fa fa-upload"></i> Enable file uploads</label>
|
|
@@ -222,19 +250,6 @@
|
|
|
222
250
|
<b>Zod docs</b><br>
|
|
223
251
|
<a href="https://zod.dev/api" target="_blank">https://zod.dev/api</a>
|
|
224
252
|
</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>
|
|
238
253
|
<div id="node-input-tip" class="form-tips"><span data-i18n="httpin.tip.in"></span><code><span id="node-input-path"></span></code>.</div>
|
|
239
254
|
</script>
|
|
240
255
|
|
|
@@ -287,7 +302,8 @@
|
|
|
287
302
|
authConfig: {type:"http.auth.config+", required:false},
|
|
288
303
|
enableZod: {value: false},
|
|
289
304
|
zodSchema: {value: ""},
|
|
290
|
-
|
|
305
|
+
cacheMode: {value: 'disabled'},
|
|
306
|
+
cacheClaim: {value: ''},
|
|
291
307
|
cacheDuration: {value: 300}
|
|
292
308
|
},
|
|
293
309
|
inputs:0,
|
|
@@ -356,11 +372,44 @@
|
|
|
356
372
|
$("#node-input-enableZod").on("change", toggleZodSchema);
|
|
357
373
|
toggleZodSchema();
|
|
358
374
|
|
|
359
|
-
|
|
360
|
-
|
|
375
|
+
var cacheModeHelp = {
|
|
376
|
+
'disabled': '',
|
|
377
|
+
'public': 'Same response is reused for all requests with the same URL and query parameters.<br>Authentication is ignored — do not use for endpoints that return user-specific data.',
|
|
378
|
+
'per-user': 'Each authenticated user receives their own cached response.<br>Responses are never shared between users.',
|
|
379
|
+
'by-claim': 'Responses are shared between users with the same value for the selected field.<br>Use fields that represent data scope (e.g. tenantId, role).'
|
|
380
|
+
};
|
|
381
|
+
function updateCacheUI() {
|
|
382
|
+
var mode = $("#node-input-cacheMode").val();
|
|
383
|
+
var help = cacheModeHelp[mode] || '';
|
|
384
|
+
$("#node-input-cache-mode-help").html(help).toggle(help !== '');
|
|
385
|
+
$(".httpin-cache-claim-row").toggle(mode === 'by-claim');
|
|
386
|
+
$(".httpin-cache-claim-help-row").toggle(mode === 'by-claim');
|
|
387
|
+
$(".httpin-cache-duration-row").toggle(mode !== 'disabled');
|
|
361
388
|
}
|
|
362
|
-
|
|
363
|
-
|
|
389
|
+
function updateAuthCacheOptions() {
|
|
390
|
+
var authNodeId = $("#node-input-authConfig").val();
|
|
391
|
+
var authNode = RED.nodes.node(authNodeId);
|
|
392
|
+
var hasAuth = authNode && authNode.authType && authNode.authType !== 'none';
|
|
393
|
+
var perUser = $("#node-input-cacheMode option[value='per-user']");
|
|
394
|
+
var byClaim = $("#node-input-cacheMode option[value='by-claim']");
|
|
395
|
+
if (!hasAuth) {
|
|
396
|
+
perUser.prop('disabled', true);
|
|
397
|
+
byClaim.prop('disabled', true);
|
|
398
|
+
var m = $("#node-input-cacheMode").val();
|
|
399
|
+
if (m === 'per-user' || m === 'by-claim') {
|
|
400
|
+
$("#node-input-cacheMode").val('disabled');
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
perUser.prop('disabled', false);
|
|
404
|
+
byClaim.prop('disabled', false);
|
|
405
|
+
}
|
|
406
|
+
updateCacheUI();
|
|
407
|
+
}
|
|
408
|
+
// Backward compat: old flows had enableCache=true, migrate to public
|
|
409
|
+
if (this.cacheMode === 'disabled' && this.enableCache === true) {
|
|
410
|
+
$("#node-input-cacheMode").val('public');
|
|
411
|
+
}
|
|
412
|
+
$("#node-input-cacheMode").on("change", updateCacheUI);
|
|
364
413
|
|
|
365
414
|
var httpRoot = (RED.settings.httpNodeRoot || "").replace(/\/$/, "");
|
|
366
415
|
var docsUrl = window.location.origin + httpRoot + "/docs";
|
|
@@ -383,8 +432,23 @@
|
|
|
383
432
|
var content = authHelpContent[type] || authHelpContent["none"];
|
|
384
433
|
$("#node-input-auth-help").html(content).show();
|
|
385
434
|
}
|
|
386
|
-
$("#node-input-authConfig").on("change",
|
|
435
|
+
$("#node-input-authConfig").on("change", function() {
|
|
436
|
+
updateAuthHelp();
|
|
437
|
+
updateAuthCacheOptions();
|
|
438
|
+
});
|
|
387
439
|
updateAuthHelp();
|
|
440
|
+
updateAuthCacheOptions();
|
|
441
|
+
},
|
|
442
|
+
oneditvalidate: function() {
|
|
443
|
+
if ($("#node-input-cacheMode").val() === 'by-claim') {
|
|
444
|
+
var claim = $("#node-input-cacheClaim").val().trim();
|
|
445
|
+
if (!claim) {
|
|
446
|
+
$("#node-input-cacheClaim").css('outline', '2px solid #d9534f');
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
$("#node-input-cacheClaim").css('outline', '');
|
|
451
|
+
return true;
|
|
388
452
|
}
|
|
389
453
|
|
|
390
454
|
});
|
package/httpin+.js
CHANGED
|
@@ -191,12 +191,29 @@ module.exports = function(RED) {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
// ── Backend cache utilities ────────────────────────────────────────────
|
|
194
|
-
function computeCacheKey(req) {
|
|
194
|
+
function computeCacheKey(req, identity) {
|
|
195
195
|
var query = req.query || {};
|
|
196
196
|
var sorted = Object.keys(query).sort().map(function(k) {
|
|
197
197
|
return encodeURIComponent(k) + '=' + encodeURIComponent(String(query[k]));
|
|
198
198
|
}).join('&');
|
|
199
|
-
|
|
199
|
+
var base = req.path + '?' + sorted;
|
|
200
|
+
if (identity) { base += '\0' + identity; }
|
|
201
|
+
return crypto.createHash('sha256').update(base).digest('hex');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function userToIdentity(user) {
|
|
205
|
+
if (user === undefined || user === null) { return null; }
|
|
206
|
+
if (typeof user === 'string') { return user; }
|
|
207
|
+
return Object.keys(user).sort().map(function(k) {
|
|
208
|
+
return k + '=' + String(user[k]);
|
|
209
|
+
}).join('|');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getCacheScope(cacheMode, cacheClaim) {
|
|
213
|
+
if (cacheMode === 'public') { return 'public'; }
|
|
214
|
+
if (cacheMode === 'per-user') { return 'user'; }
|
|
215
|
+
if (cacheMode === 'by-claim') { return 'claim:' + (cacheClaim || ''); }
|
|
216
|
+
return null;
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
function readCacheEntry(dir, key) {
|
|
@@ -281,7 +298,9 @@ module.exports = function(RED) {
|
|
|
281
298
|
this.method = n.method;
|
|
282
299
|
this.swaggerDoc = n.swaggerDoc;
|
|
283
300
|
this.enableUpload = n.enableUpload;
|
|
284
|
-
|
|
301
|
+
var legacyCacheEnabled = n.enableCache === true && !n.cacheMode;
|
|
302
|
+
this.cacheMode = n.cacheMode || (legacyCacheEnabled ? 'public' : 'disabled');
|
|
303
|
+
this.cacheClaim = (n.cacheClaim || '').trim();
|
|
285
304
|
this.cacheDuration = parseInt(n.cacheDuration) || 300;
|
|
286
305
|
this.cacheDir = path.join(RED.settings.userDir || process.cwd(), 'cache', 'http+');
|
|
287
306
|
this.uploadStorage = n.uploadStorage || 'memory';
|
|
@@ -296,26 +315,42 @@ module.exports = function(RED) {
|
|
|
296
315
|
};
|
|
297
316
|
|
|
298
317
|
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
|
-
|
|
317
318
|
// Dispatch msg downstream after successful auth
|
|
318
319
|
function dispatchMsg(user) {
|
|
320
|
+
// ── Cache hit check (runs after auth, mode-aware) ──────
|
|
321
|
+
if (node.cacheMode !== 'disabled' && node.method === 'get') {
|
|
322
|
+
var shouldCache = false;
|
|
323
|
+
var identity = null;
|
|
324
|
+
if (node.cacheMode === 'public') {
|
|
325
|
+
shouldCache = true;
|
|
326
|
+
} else if (node.cacheMode === 'per-user') {
|
|
327
|
+
identity = userToIdentity(user);
|
|
328
|
+
shouldCache = identity !== null;
|
|
329
|
+
} else if (node.cacheMode === 'by-claim') {
|
|
330
|
+
if (user && typeof user === 'object' && node.cacheClaim) {
|
|
331
|
+
var cv = user[node.cacheClaim];
|
|
332
|
+
if (cv !== undefined) { identity = String(cv); shouldCache = true; }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (shouldCache) {
|
|
336
|
+
var cKey = computeCacheKey(req, identity);
|
|
337
|
+
var hit = readCacheEntry(node.cacheDir, cKey);
|
|
338
|
+
if (hit) {
|
|
339
|
+
var scope = getCacheScope(node.cacheMode, node.cacheClaim);
|
|
340
|
+
res.set('X-Cache', 'HIT');
|
|
341
|
+
if (scope) { res.set('X-Cache-Scope', scope); }
|
|
342
|
+
if (hit.contentType) { res.set('Content-Type', hit.contentType); }
|
|
343
|
+
var hitBody = hit.bodyEncoding === 'base64'
|
|
344
|
+
? Buffer.from(hit.body, 'base64') : hit.body;
|
|
345
|
+
return res.status(hit.statusCode).send(hitBody);
|
|
346
|
+
}
|
|
347
|
+
req._httpPlusCacheKey = cKey;
|
|
348
|
+
req._httpPlusCacheTtl = node.cacheDuration;
|
|
349
|
+
req._httpPlusCacheDir = node.cacheDir;
|
|
350
|
+
req._httpPlusCacheScope = getCacheScope(node.cacheMode, node.cacheClaim);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
319
354
|
var msgid = RED.util.generateId();
|
|
320
355
|
res._msgid = msgid;
|
|
321
356
|
// Since Node 15, req.headers are lazily computed and the property
|
|
@@ -630,6 +665,9 @@ module.exports = function(RED) {
|
|
|
630
665
|
|
|
631
666
|
if (httpPlusCacheKey) {
|
|
632
667
|
headers['x-cache'] = 'MISS';
|
|
668
|
+
if (rawReq._httpPlusCacheScope) {
|
|
669
|
+
headers['x-cache-scope'] = rawReq._httpPlusCacheScope;
|
|
670
|
+
}
|
|
633
671
|
}
|
|
634
672
|
if (Object.keys(headers).length > 0) {
|
|
635
673
|
msg.res._res.set(headers);
|