HTTP In, HTTP Response, HTTP Request, and API security
An HTTP endpoint brings untrusted network input into a flow, while HTTP Request sends data to another service. You must account for route exposure, live request objects, input contracts, a response from every branch, timeouts, credentials, authentication, authorization, and direct access—not just a single successful response. This chapter covers offline checks only: do not connect or deploy anything.
Start by Defining the Four HTTP Boundaries
Add-on 22.0.1 embeds Node-RED 5.0.2. HTTP In creates a server-side route, and HTTP Response completes the same request. HTTP Request acts as a client. The API node in the HA WebSocket package provides yet another path, one that carries Home Assistant privileges. Although all of these involve APIs, their trust sources, credentials, and failure semantics differ.
| Capability | Data Direction | Primary Risks | Approach in This Chapter |
|---|---|---|---|
| HTTP In → HTTP Response | A network request enters a flow, which then responds to the caller | Unauthorized access, oversized bodies, path-parameter and header injection, missing responses | Fixed method and path, allowlist validation, a response on every path, and disabled nodes |
| HTTP Request | A flow sends a request to an external HTTPS service | SSRF, credential disclosure, redirects, unbounded responses, and timeouts | Fixed destination, credential storage, bounded timeout, and placeholders only in examples |
| HA API node | Calls an HTTP or WebSocket API through an existing HA server configuration | Reading or changing HA with the integration's privileges; dynamic methods, paths, and data broaden its capabilities | Only for advanced, approved, allowlisted use cases; this chapter neither creates nor executes calls |
| Webhook/WebSocket/TCP/UDP | Long-lived connections or other network protocols | Each has distinct authentication, source, message-size, and lifecycle concerns | Do not extrapolate HTTP rules directly; for webhooks, see Chapter 12 |
The Add-on declares host_network: true, giving the container broader access to the local network. This is a capability grant; it does not mean that any flow has established a connection. All outbound HTTP, WebSocket, TCP, and UDP destinations must be explicitly allowlisted. First use the bounded testing strategy from Chapter 18 to validate pure data logic, and only then consider integration testing.
HTTP In and Response Form One Indivisible Exchange
In Node-RED 5.0.2, HTTP In emits a message when it receives a request. For GET, msg.payload is the query object. For POST, PUT, PATCH, and DELETE, the payload is the parsed request body, or the raw data if parsing was disabled. The same message also carries msg.req and msg.res. HTTP Response needs msg.res to reply over the original connection; without it, the node only warns that no response object exists.
msg.req is the current Express request, and msg.res wraps the current response. They are not ordinary JSON objects that can be safely persisted or cloned at will. Do not place them in context, files, queues, flows that span restarts, or full Debug output, and never return either complete object to the caller.| Field | Source or Purpose | Security Interpretation |
|---|---|---|
msg.req | Live request; exposes headers, params, query, cookies, and more | Treat all of it as untrusted; project only allowlisted fields and do not retain the object |
msg.res | Live response wrapper passed along the original path to HTTP Response | Preserve it on every success, rejection, and error branch; respond only once |
msg.payload | The GET query or the body of another supported method | Successful parsing does not prove a valid shape; also validate types, lengths, ranges, and unknown fields |
msg.statusCode | Dynamic status code when HTTP Response does not specify a fixed status | Generate it only from a controlled mapping; never trust caller input directly |
msg.headers/msg.cookies | HTTP Response can merge response headers and cookies from these fields | Allowlist names and values; never reflect hop-by-hop, authentication, or unsanitized content |
The HTTP In editor supports GET, POST, PUT, DELETE, and PATCH. POST can enable file uploads, while POST, PUT, PATCH, and DELETE can skip body parsing. Uploads are stored in memory, so the route must impose limits on both size and file count. Node-RED's JSON and URL-encoded parsers use apiMaxLength; when it is not configured, the core default is 5mb. That is only a parser limit, not a sensible limit for an application's payload, and it does not cover every proxy or multipart limit.
CSV, HTML, JSON, XML, and YAML parser nodes handle syntax only. They cannot determine field-level authorization, resource cost, or whether content is trustworthy. For an external body, limit bytes and content type first, then parse it, and finally validate its shape. See Chapter 5 for the general message model.
Complete the Offline Design in Six Steps Without Starting a Route
- Write a contract for one route.
Specify one method, one relative path, the allowed content type, maximum byte count, required fields, success status, rejection status, and timeout. Use only
/PLACEHOLDER_HTTP_PATHfor the path; do not enter a real host or secret. - Map every terminal path.
Trace the normal, missing-field, wrong-type, unauthorized, internal-error, and timeout branches from HTTP In. Every branch must reach an HTTP Response node. Never place live
msg.req/msg.resobjects in Delay, context, or an unbounded path waiting for an external event. - Review the safe example offline.
This chapter's exercise is read-only: open 12-http-endpoint.json and confirm that it contains exactly 4 nodes, that HTTP In and HTTP Response both have
d:true, and that the path is entirely a placeholder. Do not import, deploy, enable, or send a test request. The README's isolated import procedure is a separate operation and never constitutes approval to deploy or connect. - Check the exposure and authentication layers.
Document Ingress and the direct host port separately. If the route might be reached through direct
/endpoint/access, plan to usehttp_node.username/passwordor a controlled reverse proxy. This Basic Auth mechanism is not fine-grained authorization. - Review the outbound design.
Read 13-outbound-http-request.json offline. Confirm that its URL is
PLACEHOLDER_HTTPS_URL, HTTP Request hasd:true, there are no authentication, TLS, or proxy configuration references, and Inject will not run at startup. - Complete a failure-acceptance table.
For each case, record the expected status, minimal body, permitted headers, external timeout, and stop condition. Network and HA nodes must remain disabled, and values must remain placeholders. Actual activation requires a change procedure outside this chapter.
Offline contract template (not executable configuration)
method: PLACEHOLDER_METHOD
path: /PLACEHOLDER_HTTP_PATH
request content type: PLACEHOLDER_MEDIA_TYPE
request max bytes: PLACEHOLDER_LIMIT
success: PLACEHOLDER_STATUS + fixed shape
reject: PLACEHOLDER_STATUS + no internal details
external I/O: do not execute
End-of-Chapter Offline Acceptance
| Deliverable | Expected Content | Stop If |
|---|---|---|
| Route contract | One method and relative path, plus media type, byte limit, input shape, authentication, authorization, success and rejection statuses, and timeout | Any field is still a guess, a real host or secret appears in the document, or interpretation requires sending a request |
| Terminal-path/status matrix | Normal, missing-field, wrong-type, unauthorized, internal-error, and timeout cases each have exactly one HTTP Response, a fixed minimal body, and a stop condition | Any path omits or duplicates a response, loses msg.res, or requires enabling the route for verification |
/endpoint, Ingress, and the Direct Port Are Three Different Concepts
The Add-on wrapper fixes the Node-RED backend at 127.0.0.1:46836, sets userDir=/config/, nodesDir=/config/nodes, and flowFile=flows.json, and sets httpNodeRoot to /endpoint. Therefore, if you enter /PLACEHOLDER_HTTP_PATH in HTTP In, the resulting Add-on path is /endpoint/PLACEHOLDER_HTTP_PATH. Do not repeat /endpoint in the node's path.
| Entry Point | Pinned-Source Behavior | Authentication and TLS Boundary |
|---|---|---|
| Ingress | The manifest specifies ingress=true, dynamic ingress_port=0, and ingress_stream=true; Ingress NGINX accepts only the Supervisor ingress source before proxying to the backend | The Supervisor path handles it; this does not imply that a direct port or custom route receives the same protection |
| Direct listener | The container's 80/tcp can map to host port 1880; whether it is exposed depends on the Supervisor port settings | ssl, certfile, and keyfile control TLS for the direct listener only; they do not issue or renew certificates automatically |
Direct / | By default, Supervisor /auth protects the editor path | Do not enable leave_front_door_open; editor authentication is not endpoint authentication |
Direct /endpoint/ | NGINX explicitly proxies directly to the backend, bypassing the editor's Supervisor auth_request | Protect it with Add-on http_node Basic Auth or a controlled proxy; authorization is still required |
| Static content | Served by the Node-RED static route | http_static.username/password protects only static content—not the editor, HTTP nodes, or Dashboard |
When http_node.username/password includes a username, the wrapper hashes the password with bundled [email protected] and passes it to Node-RED as httpNodeAuth. bcryptjs is a library supported by the wrapper, not a Palette node. Basic Auth answers only whether a caller holds the shared credentials. If users may access different resources, the flow must also authorize them from a trusted identity source, and error responses must not expose authorization details.
Every Branch Must Respond, and Must Do So Exactly Once
Every Switch output, every bounded Catch error branch, every validation-failure branch, and the normal branch must reach HTTP Response. If a branch drops the message, the caller waits until an upstream proxy or client times out. If two branches respond through the same msg.res, they cause a duplicate send. A safe structure creates a fixed error envelope first, then sets controlled msg.statusCode and header values at one response convergence point.
// Pure-data illustration; do not connect to an enabled HTTP In node
// Precondition: the upstream path still retains the original live msg.res
const value = msg.payload && msg.payload.value;
if (typeof value !== "string" || value.length < 1 || value.length > 64) {
msg.statusCode = 400;
msg.payload = { code: "INVALID_INPUT" };
return msg;
}
msg.statusCode = 200;
msg.payload = { accepted: true };
return msg;
This example illustrates only a shape guard; it does not handle identity or define a real route. A status code fixed in the HTTP Response node takes precedence. Otherwise, the node reads msg.statusCode, finally defaulting to 200. For every non-Buffer object payload, it calls Express res.jsonp(msg.payload), so a JSONP callback in the query can change the response envelope. If the contract requires invariant JSON, reserve the callback query name for security checks and reject any caller that supplies it. Alternatively, serialize trusted JSON first and set a fixed JSON content type, thereby avoiding the JSONP semantics applied to objects.
HTTP Request: Fix the HTTPS Destination; Never Let the Message Choose the Network Target
HTTP Request 5.0.2 exposes editor fields for Method (GET, POST, PUT, DELETE, HEAD, or specified by msg.method), URL, GET payload handling, TLS configuration, authentication credentials (Basic, Digest, or Bearer), keep-alive, HTTP proxy configuration, error output, the insecure HTTP parser, return type, and headers. If the node URL contains Mustache tags, the runtime renders them against the entire message, so a URL that appears in the node is not necessarily fixed. The example uses only a complete, non-templated placeholder URL without {{...}}; a production destination requires separate approval.
| Field | Recommended Boundary | Reason |
|---|---|---|
| Method/URL | Fix the method and an approved, complete HTTPS URL in the node; do not use Mustache in the URL or accept untrusted msg.url/msg.method | Prevents SSRF, internal-network probing, and unintended write methods |
| Payload | Ignore the payload for GET by default. If a query or body is needed, allowlist keys and limit lengths. For other methods, fix the content type and shape | Avoids serializing and sending a complete message or unvalidated object |
| Authentication | Use the node's credential fields; never embed credentials in a URL, Function, header constant, or exported JSON | Basic, Digest, and Bearer secrets must not enter flow source or Debug output |
| TLS | Use a controlled TLS configuration that validates the server certificate and hostname; never let a message disable validation | Without a selected TLS configuration, msg.rejectUnauthorized can still control validation, including setting it to false |
| Proxy | Use an HTTP Proxy configuration node and fixed, permitted egress; also review proxy credentials and the NO_PROXY boundary | A proxy changes the actual destination, DNS behavior, and trust-termination point |
| Parser/redirect | Set insecureHTTPParser=false; disable redirects with controlled msg.followRedirects=false, or have an egress proxy/upstream service enforce destination policy | The node has no redirect-host allowlist; following a cross-host redirect expands the SSRF surface |
| Return | Select only the required form: text, binary buffer, or parsed JSON. Enforce a response-size limit at the egress proxy/upstream service | The node has no per-response byte limit; large responses consume memory |
In addition to its configured fields, the runtime consumes msg.headers, msg.cookies, and msg.followRedirects, as well as msg.method, msg.url, and msg.requestTimeout for method, URL, and timeout control. When no TLS configuration is selected, it also consumes msg.rejectUnauthorized. Immediately before the node, therefore, create a new allowlisted message containing only the approved payload, required headers, and fixed control values. Never pass an HTTP In, Dashboard, or MQTT message through unchanged, and do not merely delete one or two known fields.
If httpRequestTimeout is not configured, the core runtime's HTTP Request timeout is 120000 ms; a positive msg.requestTimeout can override it. The implementation also permits up to 21 redirects. These are Node-RED 5.0.2 behaviors, not policies suitable for every service. Set a narrower, justified timeout for each destination, and bound both the retry count and total deadline upstream. Disable redirects, or require an egress proxy/upstream service to enforce both a destination-host allowlist and a response-size limit. Never compensate for a timeout with unlimited retries.
The core runtime first loads the entire response buffer into memory. If parsed JSON is selected, it then runs JSON.parse before emitting the message. HTTP Request itself has neither a per-response byte limit nor a redirect-host allowlist. Checking msg.responseUrl, the content type, or body size only after the node cannot prevent SSRF, cross-host redirects, or memory exhaustion that has already occurred. Still allowlist msg.statusCode, msg.headers, msg.responseUrl, msg.payload, msg.redirectList, and any msg.responseCookies on the successful output—but understand that this protects only downstream data use. Do not pass headers or cookies unchanged to HTTP Response, and do not send the complete message to Debug.
PLACEHOLDER_HTTPS_URL, embeds no authentication, and fixes HTTP Request at d:true. This is a schema for reading, not a connectable service. Do not replace the placeholder, enable the node, or deploy it.Validate the Method, Path, Body, and Headers in Layers
Selecting POST in HTTP In performs only the router layer's method match. Complete validation has at least transport, syntax, shape, authorization, and business layers. A failure at any layer must return a bounded, stable error without a stack trace, token, internal host, or raw body.
| Input Surface | Positive Allowlist | Examples to Reject |
|---|---|---|
| Method | Each route accepts only the method declared by its node | A query field requesting a switch to PUT/DELETE; retries of a non-idempotent method |
| Path/params | Fixed segments; parameters constrained by character set, length, and authorized resource scope | .., encoded separators, empty segments, overlong IDs, unauthorized object IDs |
| Query/body | Required keys, exact types, ranges, array limits, and rejection of unknown keys | null, strings instead of numbers, deeply nested objects, huge arrays, duplicate keys |
| Headers | A fixed content type and bounded Accept values; trust identity headers only after a controlled proxy has overwritten and sanitized them | Conflicting Content-Length values, dynamic Authorization, forged forwarding headers |
| Response | A fixed status mapping, content type, cache policy, and minimal body | Reflected request headers or bodies, returned exceptions, a Location derived from caller input |
CORS is not authentication. Node-RED can add CORS middleware to HTTP nodes through the global httpNodeCors setting and configure handling for OPTIONS; the HTTP In dialog has no per-route CORS field. Permit only explicit origins, methods, and headers. Do not combine a permissive wildcard with credentials. The browser same-origin policy also does not stop non-browser clients.
A reverse proxy must limit request-body size, header bytes, connection count, and upstream timeout, and it must trust only forwarding headers that it overwrites itself. Document where TLS terminates, whether the proxy-to-backend connection remains protected, and how the client IP is obtained. Add-on direct TLS covers only the direct listener; Ingress and an external proxy use different TLS sessions.
Bounded Error Model
400 INVALID_INPUT Field or type does not match the contract
401 AUTH_REQUIRED Authentication is missing or invalid
403 NOT_ALLOWED Identity is valid but lacks permission for this resource
404 NOT_FOUND Do not reveal details about absent or invisible resources
413 BODY_TOO_LARGE Reject an over-limit body before parsing
415 MEDIA_TYPE Unsupported Content-Type
429 RATE_LIMITED Bounded rate limit; no internal counter details
500 INTERNAL_ERROR Fixed minimal body; details go only to a controlled log
504 UPSTREAM_TIMEOUT Outbound deadline expired; do not retry indefinitelyThe HA API Node Is an Advanced Privileged Entry Point, Not a General HTTP Shortcut
In HA WebSocket 0.80.3, the registered persisted type is ha-api. It supports WebSocket or HTTP; the HTTP methods are GET, POST, PUT, and DELETE, and the path has its leading /api/ removed before being passed to the existing HA server configuration. WebSocket data must include type. Data may use JSON or JSONata, and the output location and type are customizable. These dynamic capabilities let one node reach many HA APIs; they do not make every call read-only.
ha-api reads overrides from msg.payload.protocol, msg.payload.method, msg.payload.path, msg.payload.data, msg.payload.dataType, the output location in msg.payload.location, the output-location type in msg.payload.locationType, the response type in msg.payload.responseType, and output properties in msg.payload.outputProperties. Fixed editor fields do not form an allowlist boundary. Immediately before the API node, delete and rebuild the entire msg.payload, adding only approved values. If that is impossible, completely isolate the node from all untrusted input. Deleting only the method and path does not block the other overrides.| Capability | Risk | Required Restriction |
|---|---|---|
| Protocol/HTTP method/path | A message can change the protocol, read data, and potentially write, delete, or trigger a state change | Rebuild the complete msg.payload, retaining only one approved protocol, method, and path; otherwise isolate all untrusted input |
WebSocket type/data | Low-level commands have broad scope, and a message can change both data and dataType | Fix the command type and exact schema; never pass a caller object directly |
| Output controls | A message can change location/locationType, responseType, and outputProperties, broadening data writes or disclosure | Include the output destination and type in the same allowlist; do not retain caller-supplied output controls |
| HA server configuration | Inherits the integration connection and token privileges | Never export the token; separate production and test configurations, and apply least privilege |
| Debug enabled/results | May expose the method, path, data, or HA response | Keep Debug disabled; emit only allowlisted results and sanitize environmental IDs such as entity, device, and area IDs |
If a Current State, Get Entities, or Action node already serves a clearly defined purpose, prefer that semantically narrower node. For the boundaries around an Action node's target and data, see Chapter 11. Use the API node only for an advanced case with no dedicated node and a fully established HA API contract. This chapter provides no executable HA API flow, method, path, or data, and does not instruct you to enable any node.
ha-webhook is an HA event entry point, not an alias for Node-RED HTTP In. A webhook URL or ID is a sensitive entry-point value and must not appear in screenshots, flow exports, or general logs. You must still constrain external sources, replay, payload size, and downstream side effects. Other WebSocket, TCP, and UDP nodes have their own configuration and framing semantics; the paired HTTP Response model does not apply to them.
For more complete operational boundaries around credentials, proxies, TLS, host networking, and least privilege, see Chapter 21. When timeout, memory, or connection problems arise, follow Chapter 22 to collect bounded diagnostic data rather than enabling complete Debug output first.
Troubleshooting
- HTTP In emits a message, but the caller keeps waiting: Trace every Switch output, validation-rejection branch, Catch branch, and normal path. Each must preserve the original
msg.resand reach HTTP Response. Do not extend the wait with a Delay node. Keep the route disabled and validate its terminal paths on a pure-data copy first. - HTTP Response reports that there is no response object: The message did not come from the same HTTP In node, or rebuilding it along the way discarded
msg.res. Do not fabricatemsg.res. Preserve the original message and overwrite only controlled fields. - Ingress works, but the direct endpoint returns 401 or is unreachable: They use different listeners and authentication boundaries. Check the host-port mapping, direct TLS, and
http_nodesettings. Never enableleave_front_door_openfor troubleshooting. - The endpoint is reachable without an editor login: Direct NGINX intentionally does not apply the editor's Supervisor authentication to
/endpoint/. Configure endpoint-specific authentication and authorization; do not treat network location as access control. - HTTP Request times out or redirects repeatedly: Keep the node disabled and check whether the placeholder is still present, along with DNS, the proxy, the TLS chain, the final host, a bounded request timeout, and redirect policy. Do not disable certificate validation or configure unlimited retries.
- Parsed JSON initially succeeds but later processing fails: A JSON parser guarantees syntax only. Add checks for required fields, types, ranges, depth, array length, and unknown fields; the error path must still respond.
- A CORS error appears only in a browser: Check the origin, preflight method and headers, and the proxy response. Do not use a wildcard with credentials, and do not treat passing CORS checks as authentication.
- The HA API node reports “requires path/type”: This is an input-contract error in an advanced node. Do not improvise values from an external payload. Keep the node disabled and return to an allowlist design with a fixed HTTP path or WebSocket type.
Pinned Version Sources and Official Documentation
This chapter uses exact commits to establish Add-on routing and Node-RED/HA node behavior. Rolling documentation provides general usage guidance only. This chapter draws no inference about proxy or environmental policy that the pinned sources do not establish.
- Home Assistant Community App: Node-RED 22.0.1 pinned commit:
/endpoint, direct/Ingress NGINX, host port,http_node,http_static, and bundled bcryptjs. - Node-RED 5.0.2 pinned commit: HTTP In/Response/Request, HTTP Proxy, TLS, parser nodes, live objects, CORS, and other network nodes.
- HA WebSocket nodes 0.80.3 pinned commit: registration and input contracts for
ha-apiandha-webhook. - Official Node-RED documentation: Core nodes.
- Official Node-RED documentation: Messages.
- Official Node-RED documentation: Securing Node-RED.
- Official Add-on documentation (pinned commit).
- Disabled examples on this site: 12-http-endpoint.json and 13-outbound-http-request.json. Read the safe-example instructions first.
Frequently Asked Questions
Can I use HTTP In without connecting it to HTTP Response?
msg.res and send it to HTTP Response, exactly once. For long-running work, define an explicitly asynchronous API contract instead of leaving the connection waiting without a bound.If I open the editor through Home Assistant Ingress, is /endpoint automatically protected by the same login?
/endpoint/ access follow different paths. The pinned direct NGINX configuration does not apply the editor's Supervisor auth_request to endpoints. You need http_node or a controlled proxy, plus separate authorization.Can I put a complete URL in msg.url so one HTTP Request node can call multiple services?
Once CORS permits a website, is API authentication complete?
Why not replace every HA node with the HA API node?
ha-api can dynamically select HTTP or WebSocket and its data, giving it a broader privilege and input surface. Dedicated nodes usually have narrower semantics and are easier to validate. Consider the advanced API node only when no dedicated capability exists and the API contract is fixed. This chapter provides no executable call.