@inteli.city/node-red-contrib-http-plus 1.0.2
Enhanced HTTP nodes for Node-RED with built-in authentication, request validation, and caching.
@inteli.city/node-red-contrib-http+
Enhanced HTTP nodes for Node-RED with built-in authentication, request validation, and caching.
What is this?
@inteli.city/node-red-contrib-http+ provides enhanced versions of Node-RED's HTTP nodes with built-in authentication, validation, and improved request handling.
These nodes are designed to be compatible with the standard Node-RED HTTP flow while adding capabilities commonly implemented manually in flows.
Key differences from standard HTTP nodes
| Feature | Standard nodes | http+ nodes |
|---|---|---|
| Authentication | Not built-in | Built-in (Basic + Cognito) |
| Request validation | Manual (function nodes) | Built-in (Zod) |
| File upload handling | Basic | Structured (msg.files) |
| Data normalization | Manual | Built-in via Zod transforms |
| Streaming responses | Limited/manual | First-class support |
| Backend caching | Not available | Built-in (http.in+) |
| Browser cache control | Manual headers | Built-in (http.out+) |
Compatibility
http.in+andhttp.out+follow the same flow model as the standard nodeshttp.request+is a drop-in replacement for the standardhttp requestnode- Existing flows can be migrated incrementally
Standard: http in ──→ function ──→ http response
With http+: http.in+ ──→ (validated + authenticated) ──→ http.out+
When to use http+ nodes
Use http+ when you need:
- Authentication at the HTTP boundary
- Input validation without extra function nodes
- Cleaner and more predictable request handling
Use standard nodes when:
- You need minimal setup
- You are prototyping quickly without constraints
Index
- Nodes
- Install
- Core Concepts
- Caching
- Authentication
- Validation with Zod
- Swagger / OpenAPI
- File uploads
- http.request+
- http.out+
- Best practices
Nodes
| Node | Role |
|---|---|
http.in+ |
Receives HTTP requests. Runs auth, Zod validation, and optional backend caching before sending msg downstream. |
http.request+ |
Makes outgoing HTTP requests. Drop-in replacement for the standard http request node. |
http.out+ |
Sends the HTTP response back to the caller. Optionally controls browser caching via Cache-Control. |
Install
cd ~/.node-red
npm install @inteli.city/node-red-contrib-http+
Core Concepts
Message properties
| Property | Contains |
|---|---|
msg.payload |
Request body (POST/PUT/PATCH) or query object (GET) |
msg.req.query |
Query string as object — GET /search?term=abc → { term: "abc" } |
msg.req.params |
Route parameters — GET /users/42 → { id: "42" } |
msg.req.headers |
Request headers |
msg.validated |
Parsed + validated object from Zod (only when validation passes) |
msg.user |
Mapped user identity. For Basic Auth: the username string. For Cognito: fields mapped from the JWT payload (only when "Expose user to flow" is enabled). |
msg.res |
Response handle — passed to http.out+ to send the reply |
Basic flow
http.in+ ──→ [your logic] ──→ http.out+
inject ──→ http.request+ ──→ debug
Caching
http+ supports two distinct caching mechanisms that operate at different layers and solve different problems.
| Feature | Backend cache (http.in+) |
Browser cache (http.out+) |
|---|---|---|
| Layer | Server | Client (browser) |
| When applied | Before flow execution | After response is sent |
| Effect | Skips the flow entirely on a hit | Browser may reuse the response |
| Default | Disabled | Disabled |
| Risk | Serving stale or incorrect data | Stale data in the browser |
They can be used independently or combined.
How they work
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.
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.
Backend cache (http.in+)
Caches full HTTP responses on the server. Repeated identical GET requests are served from cache without executing any downstream nodes.
Active only for GET requests. The Cache field controls the scope:
| Mode | Who shares the cache | Auth required |
|---|---|---|
| Disabled | — | — |
| Public | All requests with the same URL + query | No |
| Per-user | Each authenticated user has their own entry | Yes |
| By claim | Users sharing the same value for a chosen identity field | Yes |
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.
Storage — responses are stored on disk:
<userDir>/cache/http+/
The directory is created automatically if it does not exist.
TTL — controlled by the "Cache duration (s)" field. Expired entries are ignored and replaced on the next request.
Observability — every cached response includes:
X-Cache: HIT → served from cache; flow was skipped
X-Cache: MISS → cache was empty or expired; flow ran
X-Cache-Scope: public | user | claim:<field>
Examples:
GET /products → Public cache (shared across all callers)
GET /profile → Per-user cache (isolated per authenticated user)
GET /dashboard → By claim: tenantId (shared per tenant, isolated between tenants)
Browser cache (http.out+)
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.
What it does:
When enabled, http.out+ adds the following header to every response:
Cache-Control: public, max-age=<duration>
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.
Example:
[http.in+ GET /logo.png] ──→ [read file] ──→ [http.out+ browser cache: 86400 s]
The browser caches the image for 24 hours. On subsequent page loads, no network request is made.
⚠️ When NOT to use browser cache
- Dynamic responses that change between requests
- User-specific or session-specific data
- Any endpoint where a stale cached response would cause incorrect behavior
Using both together
Backend cache and browser cache can be combined on the same endpoint:
[http.in+ GET /summary cache: 60 s] ──→ [compute] ──→ [http.out+ browser cache: 60 s]
- The server caches the response for 60 seconds — the flow is skipped on repeated hits.
- The browser also caches it for 60 seconds — the request may not leave the client at all.
Align both TTLs to avoid a situation where the browser caches a response longer than the server would serve the same stale version.
For highly dynamic data, leave both disabled.
Authentication
Configure authentication by creating an http.auth.config+ config node and attaching it to http.in+.
| Type | Behaviour |
|---|---|
| None | All requests pass through |
| Basic Auth | Validates Authorization: Basic … header. Browser login popup triggered automatically via WWW-Authenticate. Supports single-user and multi-user modes (see below). |
| Cognito JWT | Validates a Bearer token against a JWKS endpoint. Token accepted from Authorization: Bearer <token> or ?token=. JWKS keys are cached for 1 hour. |
Failed authentication returns 401 and stops the flow.
Basic Auth modes
Single-user (default): Enter a username and password directly in the config node. Credentials are stored securely via Node-RED's credentials system (not in the exported flow).
Multiple users (JSON): Enable the "Use multiple users" option and provide a JSON object mapping usernames to passwords:
{
"admin": "password123",
"user": "abc123"
}
Warning: Passwords in JSON mode are stored in plain text in the flow configuration. Use only in trusted environments.
JWKS caching
Cognito public keys (JWKS) are cached in memory to reduce latency and avoid repeated requests to AWS.
- Cache duration: 1 hour
- Keys are cached per
kid(key ID) - A new key is fetched automatically if an unknown
kidis received
This improves performance while still supporting key rotation. If AWS rotates signing keys, new keys will be picked up automatically after cache expiration or when a new kid appears.
Cognito user mapping
After successful JWT validation, the Authorization header and ?token= query parameter are always removed from the request before it enters the flow.
To expose user identity in msg.user, enable "Expose user to flow" in the config node and provide a JSON mapping:
{
"id": "sub",
"email": "email",
"roles": "cognito:groups"
}
Keys are the output field names in msg.user; values are the JWT claim names to read from. Fields missing from the token are set to undefined.
If "Expose user to flow" is disabled, msg.user is not set — no JWT data propagates into the flow.
Request sanitization
After successful authentication, credentials are removed from the request before dispatch:
| Auth type | Removed |
|---|---|
| Basic Auth | msg.req.headers.authorization |
| Cognito JWT | msg.req.headers.authorization, msg.req.query.token |
This prevents tokens and passwords from leaking into downstream nodes.
Validation with Zod
http.in+ can validate every incoming request before passing it downstream.
Enabling validation
- Open the
http.in+node editor. - Check Enable Zod validation.
- Enter a Zod schema expression in the Schema textarea.
The schema receives a single object:
{
body: // request body (msg.payload)
query: // query string parameters (msg.req.query)
params: // route parameters (msg.req.params)
files: // uploaded file metadata (when file upload is enabled)
}
Use the appropriate field depending on the HTTP method:
| Method | Use |
|---|---|
| GET, DELETE | query, params only — no body |
| POST, PUT, PATCH | body, query, params |
Defining
bodyin a GET or DELETE schema is ignored at runtime. A warning is logged at deploy time.
Full usage guidance (method table, type coercion, Swagger behavior) is available inline in the node editor UI.
If validation passes, msg.validated contains the parsed result and the flow continues normally.
If validation fails, the node returns HTTP 400 immediately — no downstream node is executed:
{
"error": "invalid_request",
"details": [
{ "code": "invalid_type", "path": ["body", "age"], "message": "Expected number, received string" }
]
}
Important: query and param values are always strings
HTTP query strings and route params arrive as strings. Use z.coerce to convert them:
z.coerce.number() // "42" → 42
z.coerce.boolean() // "true" → true
Transforms and normalization
Zod can transform values as part of validation — trimming whitespace, converting case, coercing types, etc. The transformed result is what ends up in msg.validated. msg.payload always remains the original, unmodified request body.
z.object({
query: z.object({
term: z.string().trim().toLowerCase(),
page: z.coerce.number().int().min(1)
})
})
The flow receives msg.validated.query.term already trimmed and lowercased — no manual cleanup needed.
File validation
Zod validates file metadata (mime type, size, etc.), not file content. Files from the upload are available in msg.files as an array — validate the first entry or a named field:
z.object({
files: z.object({
avatar: z.object({
mimetype: z.enum(["image/png", "image/jpeg"]),
size: z.number().max(2_000_000)
})
})
})
Examples
Example 1 — Body validation (POST)
Route: POST /users
Schema:
z.object({
body: z.object({
name: z.string().min(1),
age: z.number().int().min(0)
})
})
Valid request body:
{ "name": "Alice", "age": 30 }
Invalid — returns 400:
{ "name": "", "age": -1 }
Example 2 — Query validation (GET)
Route: GET /search?term=node-red&page=2
Schema:
z.object({
query: z.object({
term: z.string().min(1),
page: z.coerce.number().int().min(1).optional()
})
})
page arrives as a string from the URL — z.coerce.number() converts it automatically.
Example 3 — Route parameter validation
Route: GET /users/:id
Schema:
z.object({
params: z.object({
id: z.string().uuid()
})
})
Rejects any request where :id is not a valid UUID before your logic runs.
Example 4 — Combined: params + query + body
Route: PATCH /users/:id?notify=true
Schema:
z.object({
params: z.object({
id: z.string().uuid()
}),
query: z.object({
notify: z.coerce.boolean().optional()
}).optional(),
body: z.object({
email: z.string().email().optional(),
role: z.enum(["admin", "user", "viewer"]).optional()
})
})
msg.validated will contain the fully parsed and typed object for use downstream.
Validation error format
{
"error": "invalid_request",
"details": [
{
"code": "invalid_type",
"path": ["body", "age"],
"message": "Expected number, received string"
},
{
"code": "too_small",
"path": ["body", "name"],
"message": "String must contain at least 1 character(s)"
}
]
}
details is the raw Zod ZodError.errors array. Each entry contains a path array indicating exactly which field failed.
Best practices
| Scenario | Recommendation |
|---|---|
| Required identifiers | Use route params (:id) with z.string().uuid() or similar |
| Filters and options | Use query string with z.coerce for number/boolean |
| Structured input data | Use request body (body) — POST/PUT/PATCH only |
| GET / DELETE filtering | Use query — never body |
| All query/param values | Always use z.coerce.* — they arrive as strings |
| Optional sections | Wrap entire query or params in .optional() if the route doesn't always have them |
| Schema errors at deploy | The node logs the error and disables validation — it does not crash |
| Swagger visibility | Only nodes with a valid Zod schema appear in /docs |
Swagger / OpenAPI
http.in+ nodes with a valid Zod schema are automatically registered in an OpenAPI 3.0 spec. No extra configuration is required.
| Endpoint | Returns |
|---|---|
GET /openapi.json |
Raw OpenAPI spec (JSON) |
GET /docs |
Swagger UI |
Only endpoints where Zod validation is enabled and the schema compiled successfully appear in the spec. The spec updates automatically on each deploy — no stale entries.
For schema field usage, method rules, and type coercion guidance, see the inline help panel in the node editor.
File uploads
http.in+ supports multipart (multipart/form-data) file uploads on POST routes. The upload option is not available for GET, PUT, PATCH, or DELETE.
If file upload is enabled, uploaded files are available in msg.files — they are not included in msg.payload.
Enabling uploads
- Open the
http.in+node editor. - Check Enable file uploads.
- Choose Storage:
Memory (buffer)orDisk (temp files). - Set Max size (MB) (default: 5 MB).
- For disk storage, optionally set a Temp dir (defaults to the OS temp directory).
msg.files
When files are uploaded, msg.files is an array of objects:
| Property | Present | Contains |
|---|---|---|
fieldname |
always | Form field name used for the upload |
originalname |
always | Original filename from the client |
mimetype |
always | MIME type (e.g. image/png) |
size |
always | File size in bytes |
storage |
always | "memory" or "disk" |
buffer |
memory only | Buffer containing the file data |
path |
disk only | Absolute path to the saved file |
Size limit
If a file exceeds the configured limit, the node returns 413 immediately:
{ "error": "file_too_large" }
Disk storage — cleanup
When using disk storage, files are written to the temp directory and not deleted automatically. Your flow is responsible for removing them after use (e.g. via a function node calling fs.unlink).
http.request+
Drop-in replacement for the standard http request node with identical behaviour.
Input message properties used:
| Property | Effect |
|---|---|
msg.url |
Target URL (overrides node config) |
msg.method |
HTTP method (when node is set to "use msg.method") |
msg.payload |
Request body |
msg.headers |
Additional request headers |
Output message properties set:
| Property | Contains |
|---|---|
msg.payload |
Response body |
msg.statusCode |
HTTP status code |
msg.headers |
Response headers |
msg.responseUrl |
Final URL after redirects |
msg.redirectList |
List of redirects followed |
Supports TLS config, proxy config (http.proxy+), Basic/Digest/Bearer auth, and persistent connections.
http.out+
Sends the HTTP response back to the original caller. Must be connected to the same flow started by http.in+.
Controlling the response via msg:
| Property | Effect |
|---|---|
msg.payload |
Response body |
msg.statusCode |
HTTP status code (default: 200) |
msg.headers |
Extra response headers |
msg.cookies |
Cookies to set or clear |
If msg.payload is an object, the response is sent as JSON automatically.
Browser caching
http.out+ can automatically set browser cache headers without requiring a function node.
Enable Enable browser cache in the node editor and set Cache duration (s). The node will add:
Cache-Control: public, max-age=<duration>
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.
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.
Use for static or infrequently-changing responses (images, scripts, reference data). Avoid for user-specific or time-sensitive responses.
For a full explanation including TTL guidance, safety rules, and combining with backend cache, see the Caching section.
Streaming responses
You can stream a response instead of sending a buffer. Useful for large files or when proxying a stream from another source.
Set msg.stream to any readable Node.js stream:
msg.stream = fs.createReadStream("/tmp/file.zip");
If msg.stream is set, the stream is piped directly to the response. If it is not set, msg.payload is sent normally.
msg.headers and msg.statusCode behave the same in both modes.
File structure
httpproxy+.js/.html — http.proxy+ config node (proxy for http.request+); includes proxy resolution utility
httpin+.js/.html — http.in+ and http.out+ nodes
httprequest+.js/.html — http.request+ node
http-auth-config+.js/.html — http.auth.config+ config node
libs/swagger.js — OpenAPI spec generation and /docs + /openapi.json route registration