npm package: @inteli.city/node-red-contrib-http-plus
Description: Enhanced HTTP nodes for Node-RED with built-in authentication, request validation, and caching.
Latest version: 1.0.2

# @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+` and `http.out+` follow the same flow model as the standard nodes
- `http.request+` is a drop-in replacement for the standard `http request` node
- 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](#nodes)
- [Install](#install)
- [Core Concepts](#core-concepts)
- [Caching](#caching)
- [Authentication](#authentication)
- [Validation with Zod](#validation-with-zod)
- [Swagger / OpenAPI](#swagger--openapi)
- [File uploads](#file-uploads)
- [http.request+](#httprequest)
- [http.out+](#httpout)
- [Best practices](#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

```bash
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:

```json
{
  "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 `kid` is 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:

```json
{
  "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

1. Open the `http.in+` node editor.
2. Check **Enable Zod validation**.
3. Enter a Zod schema expression in the **Schema** textarea.

The schema receives a single object:

```js
{
  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 `body` in 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:

```json
{
  "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:

```js
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.

```js
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:

```js
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:**
```js
z.object({
  body: z.object({
    name: z.string().min(1),
    age:  z.number().int().min(0)
  })
})
```

**Valid request body:**
```json
{ "name": "Alice", "age": 30 }
```

**Invalid — returns 400:**
```json
{ "name": "", "age": -1 }
```

---

### Example 2 — Query validation (GET)

**Route:** `GET /search?term=node-red&page=2`

**Schema:**
```js
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:**
```js
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:**
```js
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

```json
{
  "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

1. Open the `http.in+` node editor.
2. Check **Enable file uploads**.
3. Choose **Storage**: `Memory (buffer)` or `Disk (temp files)`.
4. Set **Max size (MB)** (default: 5 MB).
5. 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:

```json
{ "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](#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:

```js
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
```