@inteli.city/node-red-contrib-http-plus 1.0.8
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 with Nunjucks-templated headers and body, request queuing, and optional auth. |
http.out+ |
Sends the HTTP response back to the caller. Optionally controls browser caching via Cache-Control. |
http.auth.out+ |
Config node — manages outgoing authentication (Basic Auth, JWT from endpoint) for http.request+. |
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+)
In the node editor, backend cache is labelled "Server cache" to distinguish it from browser cache.
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>/.runtime_data/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 /docs/openapi.json |
Raw OpenAPI spec (JSON). Swagger UI loads it via a relative URL so it works behind reverse-proxy path prefixes. |
GET /docs |
Redirects to /docs/ |
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.
What gets documented
| Zod key | OpenAPI output |
|---|---|
body |
requestBody → reusable schema in components.schemas |
files (uploads enabled) |
requestBody multipart → reusable schema in components.schemas |
query |
parameters (in: query) — inline, not a reusable schema |
params |
parameters (in: path) — inline, not a reusable schema |
body and files schemas are promoted to components.schemas so Swagger UI displays them under the Schemas section and operations link back to them via $ref. Query and path parameters stay as inline parameter objects — converting them into shared schemas would distort their HTTP semantics without any benefit.
Schema naming
Schema names are derived from the HTTP method and path — they are stable and deterministic across restarts and redeploys:
POST /users → PostUsersBody
POST /users/{id}/avatar → PostUsersIdAvatarForm (file upload)
PUT /products/:name → PutProductsNameBody
PATCH / → PatchRootBody
Type representation
Schemas always reflect post-validation (semantic) types, not the raw HTTP transport type:
z.coerce.number() → { type: "number" } (not "string")
z.coerce.boolean() → { type: "boolean" } (not "string")
Full example
Route: POST /users
Zod schema:
z.object({
body: z.object({
name: z.string(),
email: z.string(),
age: z.coerce.number().optional()
}),
query: z.object({
dry_run: z.coerce.boolean().optional()
}),
params: z.object({})
})
Generated /docs/openapi.json (simplified):
{
"paths": {
"/users": {
"post": {
"parameters": [
{
"name": "dry_run",
"in": "query",
"required": false,
"schema": { "type": "boolean" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/PostUsersBody" }
}
}
},
"responses": { "200": { "description": "OK" } }
}
}
},
"components": {
"schemas": {
"PostUsersBody": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"age": { "type": "number" }
},
"required": ["name", "email"]
}
}
}
}
age is absent from required because it is .optional(). dry_run is a query parameter, not a schema entry. Swagger UI renders PostUsersBody in the Schemas section and links to it from the operation.
File upload example
Route: POST /documents (file uploads enabled)
Zod schema:
z.object({
files: z.object({
document: z.any(),
thumbnail: z.any().optional()
}),
body: z.object({
title: z.string()
})
})
Generated schema (PostDocumentsForm):
{
"type": "object",
"properties": {
"document": { "type": "string", "format": "binary" },
"thumbnail": { "type": "string", "format": "binary" },
"title": { "type": "string" }
},
"required": ["document", "title"]
}
File and body fields are merged into a single multipart/form-data schema. File fields are always represented as { type: "string", format: "binary" }.
What does NOT appear in the spec
- Endpoints with Zod disabled
- Endpoints where the schema failed to compile
- Response schemas (out of scope —
http.out+does not guarantee response shape) - Query and path parameter schemas (they remain as parameters)
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+
Makes outgoing HTTP requests with Nunjucks-templated headers and body, configurable concurrency, and optional outgoing authentication via http.auth.out+.
Inputs
| Property | Type | Description |
|---|---|---|
msg.url |
string | Target URL — overrides the node URL when set. Supports Mustache syntax |
msg.method |
string | HTTP method when the node is configured to use msg.method |
msg.stop |
boolean | Set to true to cancel all running and queued requests |
Outputs
| Property | Type | Description |
|---|---|---|
msg.payload |
string | object | buffer | Response body (format controlled by Return) |
msg.statusCode |
number | HTTP response status code |
msg.headers |
object | Response headers |
msg.responseUrl |
string | Final URL after any redirects |
Templates
Body and Headers are Nunjucks templates rendered against the full msg object at runtime. Objects coerce to JSON automatically when interpolated — no | dump filter needed.
{{ payload }} → msg.payload, JSON if it is an object
{{ payload.id }} → nested scalar, raw value
{{ topic }} → msg.topic
Body
Sent for POST, PUT, and PATCH only. If the rendered output is valid JSON it is sent with Content-Type: application/json; otherwise as a plain string. Leave empty to send no body.
{{ payload }}
{"id": {{ payload.id }}, "event": "{{ topic }}"}
Headers
Must render to a JSON object. Use {} to send no custom headers.
Static authentication (Bearer tokens, API keys) belongs here:
{"Authorization": "Bearer {{ payload.token }}"}
{"X-API-Key": "abc123"}
Authentication
Attach an http.auth.out+ config node via the Auth field for authentication requiring lifecycle management (credential storage, automatic token refresh). For static tokens or keys with no lifecycle needs, define them in the Headers template directly.
Header merge order: auth-generated headers are applied first; Headers template values are applied after and take precedence. A user-defined Authorization header in the Headers template silently overrides the auth-generated one.
Concurrency and queue
The Queue setting (default: 1) controls how many requests run in parallel. Incoming messages beyond that limit are held in a FIFO queue.
To abort all pending and in-flight requests:
- Send
msg.stop = true - Click the kill button (⏹) in the node editor
http.auth.out+
Config node that manages outgoing authentication for http.request+. Attach it via the Auth field in the request node editor.
Authentication types
None
No authentication. No headers added.
Basic Auth
Stores username and password using Node-RED's secure credentials system. Adds on every request:
Authorization: Basic <base64(username:password)>
Credentials are never included in exported flow definitions.
JWT (from endpoint)
Fetches an access token from a token URL, caches it, and adds:
Authorization: Bearer <token>
| Field | Description |
|---|---|
| Token URL | URL of the token endpoint |
| Method | HTTP method for the token request (POST, GET, PUT) |
| Username / Password | Stored securely via Node-RED credentials. Available in Body and Headers templates as credentials.username and credentials.password |
| Body | Nunjucks template for the token request body (POST / PUT only). Context: msg + credentials.* |
| Headers | Nunjucks template for token request headers. Must render to a JSON object |
| Token field | Key to read from the token endpoint response (default: access_token) |
| Fallback TTL | Cache duration in seconds when expires_in is absent or exceeds this value (default: 1200) |
Body template example:
{"username": "{{ credentials.username }}", "password": "{{ credentials.password }}"}
The credentials namespace is only available within JWT token request templates — it is not injected into the main request.
Token caching
Tokens are cached in memory per config node instance. Effective TTL is min(expires_in, fallback TTL) with a 30-second safety margin. Concurrent fetches are deduplicated — if multiple requests arrive while a fetch is in progress they all wait for the same result. Cache is reset on redeploy.
401 auto-refresh
On a 401 from the target API, the cache is invalidated and a new token is fetched. The request is retried once with the fresh token. If the retry also returns 401, or the token fetch fails, a warning is emitted via node.warn and the response is passed through unchanged.
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/ + /docs/openapi.json route registration