Skip to Content

One request comes in, another goes out, and neither should ever pick its own destination

a door in, a line out, no guessing who's on either end
Node-RED Guide · Part 10

One request comes in, another goes out, and neither should ever pick its own destination

This opens the engineering and operations stretch of the guide, and it starts with the part of Node-RED that talks to the outside world. HTTP In opens a route into your flow. HTTP Response is the only node that can finish that same request. HTTP Request is the opposite direction — your flow calling out to someone else's service. And there is a fourth, quieter path: the API node bundled with the Home Assistant integration, which reaches HA directly with whatever privileges that integration's token already holds. All four get called “an API” in casual conversation. This chapter is about not treating them as interchangeable, because what each one trusts, what it needs to authenticate, and what happens when it fails are all different. Nothing here gets deployed. You are writing a contract and reading two already-disabled example flows — the wiring itself is a decision for after you have both in hand.

5mb
Node-RED's core default for the JSON and URL-encoded body parsers, when apiMaxLength is left unset. A parser limit, not a sensible limit for your route
120000 ms
HTTP Request's timeout when httpRequestTimeout is not configured — two minutes, unless a positive msg.requestTimeout overrides it
21
Redirects HTTP Request will follow by default, with no allowlist over which host any of them leads to
Four doors, one habit

Start by telling these four capabilities apart, before you use any of them

Add-on 22.0.1 bundles Node-RED 5.0.2. HTTP In opens a route that lives on your Node-RED backend, and HTTP Response is the only node that can finish that same request. HTTP Request runs the other way: your flow acting as a client, calling out to someone else's HTTPS service. The fourth capability is easy to overlook because it does not look like networking at all — the API node bundled with the HA WebSocket integration, which reaches Home Assistant directly and carries whatever privileges that integration's own connection already has. All four involve something you could reasonably call an API. Their trust source, their credentials, and what happens when they fail do not have that much in common.

CapabilityData directionMain risksThis chapter's approach
HTTP In → HTTP ResponseA network request enters a flow, which then answers the callerUnauthorized access, oversized bodies, path or header injection, a branch that never respondsFixed method and path, allowlist validation, a response on every branch, and the route disabled until it is reviewed
HTTP RequestA flow sends a request to an external HTTPS serviceSSRF, credential disclosure, redirects, an unbounded response, and no built-in timeoutFixed destination, credentials kept in the node's own fields, a bounded timeout, placeholders only in examples
HA API nodeCalls an HTTP or WebSocket API through an existing HA server configurationReads or changes Home Assistant with the integration's own privileges; dynamic method, path, and data widen that furtherReserved for an advanced, approved, allowlisted case — this chapter creates and enables nothing
Webhook, WebSocket, TCP, UDPLong-lived connections or other protocols entirelyEach has its own authentication, source, message-size, and lifecycle rulesDo not carry HTTP's rules over by assumption; a webhook is covered in the events chapter earlier in the guide

The Add-on also declares host_network: true, which gives the container broader access to the local network than a typical add-on gets.

In plain terms

host_network: true is a capability grant, not a connection. It is the difference between being handed a master key to every door in the building and having actually opened one of them. Nothing walks through a door until a flow is built, deployed, and pointed at a specific outbound address — and every one of those addresses still has to be on your own allowlist before you get there.

If you have not already, it is worth returning to the bounded testing habit from the debugging chapter before you go further: confirm your validation logic on pure, disconnected data first, and only bring the network into it once that logic is already proven.

flowchart LR
  A["A caller somewhere
on the network"] --> B["HTTP In receives the request;
HTTP Response answers it"] C1["A flow you built"] --> D["HTTP Request sends to
an external HTTPS service"] C2["A flow you built"] --> E["HA API node calls Home Assistant
with the integration's own privileges"] F["Webhook, WebSocket,
TCP, or UDP"] -.->|"its own rules,
not this chapter"| G["A different lifecycle entirely"]
Four capabilities, four outcomesFour chains leave the left-hand column, one per capability from the table above. Three of them run through a solid arrow the whole way; only the Webhook/WebSocket/TCP/UDP chain ends in a dotted one, because this chapter's HTTP In/Response and HTTP Request rules do not extend to it — it keeps its own authentication and its own lifecycle.
One exchange, two live objects

HTTP In and HTTP Response are one exchange, not two separate nodes

HTTP In emits a message the instant it receives a request. For a 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 you disabled parsing. Alongside the payload, the same message quietly carries two more fields: msg.req and msg.res. HTTP Response needs msg.res to answer over the original connection. Without it, the node only warns that no response object exists; it cannot invent one.

Live-object boundary. msg.req is the current Express request, and msg.res wraps the current response. Neither is an ordinary JSON object you can safely persist or clone. Do not place either one in context, in a file, on a queue, in a flow that spans a restart, or in full Debug output — and never return the complete object to the caller.
In plain terms

It is the difference between a customer standing at your counter, mid-conversation, and a photograph you took of them. The photograph you can keep, print, and email to whoever you like. The customer standing there is not something you put in a drawer for later — the moment you try to store “the customer” instead of what they actually said, the whole conversation stops making sense. msg.req and msg.res are the customer, not the photograph.

FieldSource or purposeSecurity interpretation
msg.reqThe live request; exposes headers, params, query, cookies, and moreTreat all of it as untrusted. Project out only allowlisted fields, and do not retain the object itself
msg.resThe live response wrapper, carried along the original path to HTTP ResponsePreserve it on every success, rejection, and error branch. Respond through it exactly once
msg.payloadThe GET query, or the body under another supported methodSuccessful parsing proves nothing about shape — still check type, length, range, and unknown fields
msg.statusCodeA dynamic status code, when HTTP Response has no fixed status of its ownGenerate it only from a controlled mapping. Never trust caller input directly
msg.headers / msg.cookiesHTTP Response can merge these into the response headers and cookiesAllowlist names and values. Never reflect hop-by-hop, authentication, or unsanitized content back out

In the HTTP In editor you can pick GET, POST, PUT, DELETE, or PATCH. POST can turn on file uploads, and POST, PUT, PATCH, and DELETE can all skip body parsing entirely. Uploads land in memory, so the route needs its own limit on both size and file count — do not assume one exists by default. Node-RED's JSON and URL-encoded parsers use apiMaxLength, and when it is not set, the core default is 5mb. That is only a parser ceiling. It is not a sensible limit for your application's actual payload, and it covers neither a proxy's own limit nor a multipart upload.

The CSV, HTML, JSON, XML, and YAML parser nodes handle syntax only, the same way they were introduced back in the messages chapter. None of them can tell you whether a field was authorized, whether the resource cost is reasonable, or whether the content itself deserves to be trusted. For anything arriving from outside, the order is always the same: limit bytes and content type first, parse second, and validate the shape last.

Design it before you deploy it

Design the route on paper, in six steps, before you deploy anything

For a route that has real consequences, writing the contract down first is not paperwork for its own sake. It is how you notice a missing error branch, or a byte limit you never actually set, while fixing it still costs you nothing. The six steps below are the version of that habit this chapter asks for — and every one of them can be done with the route sitting disabled.

  1. Step 1

    Write a contract for one route

    Specify one method, one relative path, the allowed content type, a maximum byte count, the required fields, the success status, the rejection status, and a timeout. Use only /PLACEHOLDER_HTTP_PATH for the path — never a real host or a real secret.

  2. Step 2

    Map every terminal path

    Trace the normal, missing-field, wrong-type, unauthorized, internal-error, and timeout branches leaving HTTP In. Every one of the six has to reach HTTP Response. Never park a live msg.req or msg.res in a Delay node, in context, or on a path that waits on an external event with no bound.

  3. Step 3

    Review the safe example offline

    This chapter's exercise is read-only: open the site's disabled example, 12-http-endpoint.json, and confirm it holds exactly four nodes, that HTTP In and HTTP Response both carry d:true, and that the path is entirely a placeholder. Do not import it, deploy it, enable it, or send it a test request. Reading the site's own isolated-import instructions is a separate step, and never counts as approval to deploy or connect anything.

  4. Step 4

    Check the exposure and authentication layers

    Document Ingress and the direct host port as two separate things. If the route might be reached through a direct /endpoint/ path, plan on http_node.username/password or a controlled reverse proxy. Remember that this Basic Auth mechanism is not fine-grained authorization — it is next.

  5. Step 5

    Review the outbound design

    Read the paired offline example, 13-outbound-http-request.json. Confirm its URL is PLACEHOLDER_HTTPS_URL, that HTTP Request carries d:true, that there are no authentication, TLS, or proxy references anywhere in it, and that Inject will not fire at startup.

  6. Step 6

    Complete a failure-acceptance table

    For each case, record the expected status, the minimal body, the permitted headers, the external timeout, and the stop condition. Network and HA nodes stay disabled throughout, and every value stays a placeholder. Turning any of it on is a separate change, 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
Ready to buildOne method, a path that is still /PLACEHOLDER_HTTP_PATH, a written byte limit, and a defined response for all six branches from Step 2.
Not yetA path that is already a real, working address, no byte limit written down anywhere, and one branch you are planning to “come back to.”

End-of-chapter offline acceptance

DeliverableExpected contentStop if
Route contractOne method and relative path, plus media type, byte limit, input shape, authentication, authorization, success and rejection statuses, and a timeoutAny field is still a guess, a real host or secret appears in the document, or answering it requires actually sending a request
Terminal-path matrixNormal, missing-field, wrong-type, unauthorized, internal-error, and timeout cases each have exactly one HTTP Response, a fixed minimal body, and a stop conditionAny path omits or duplicates a response, loses msg.res, or requires enabling the route to verify
Three ways in

Ingress, the direct port, and /endpoint protect the route in three different ways

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. So if you type /PLACEHOLDER_HTTP_PATH into HTTP In, the path the Add-on actually exposes is /endpoint/PLACEHOLDER_HTTP_PATH. Do not type /endpoint again inside the node's own path field — it is already there.

Entry pointPinned-source behaviorAuthentication and TLS boundary
IngressThe manifest sets ingress=true, a dynamic ingress_port=0, and ingress_stream=true; Ingress NGINX accepts only the Supervisor's own ingress source before proxying to the backendThe Supervisor path handles it. That does not mean a direct port or a custom route gets the same protection
Direct listenerThe container's 80/tcp can map to host port 1880; whether it is actually exposed depends on your Supervisor port settingsssl, certfile, and keyfile control TLS for the direct listener only. They do not issue or renew a certificate for you
Direct /By default, Supervisor /auth protects the editor pathDo not enable leave_front_door_open. Editor authentication is not the same thing as endpoint authentication
Direct /endpoint/NGINX proxies straight to the backend here, bypassing the editor's Supervisor auth_request entirelyProtect it yourself with Add-on http_node Basic Auth or a controlled proxy — authorization is still a separate step
Static contentServed by Node-RED's own static routehttp_static.username/password protects only static content, not the editor, HTTP nodes, or a Dashboard
flowchart TD
  A["A request arrives at the Add-on"] --> B{"Which of the three
paths did it use?"} B -->|"Ingress"| C["Ingress NGINX accepts only
the Supervisor ingress source"] B -->|"Direct editor path, /"| D["Supervisor /auth protects it,
unless leave_front_door_open is on"] B -->|"Direct /endpoint/"| E["NGINX proxies straight to
the backend, skipping
Supervisor auth_request"] E --> F["Needs its own guard:
http_node username/password,
or a controlled proxy"]
One diamond, three branches, one branch that needs moreAll three branches leave the same decision diamond. Ingress and the direct editor path each end at the box they reach. Only the direct /endpoint/ branch continues on to a second box, because it is the one path NGINX does not run through the editor's Supervisor login — so it needs a guard configured for it specifically.

When http_node.username/password includes a username, the wrapper hashes the password with its bundled bcryptjs library and passes the result to Node-RED as httpNodeAuth. That library is a wrapper dependency, not a node you drag into a flow.

In plain terms

Basic Auth is the key to the building's front door. Everyone who holds a copy gets in the same way, and the guard checking it only asks one question: do you have the key at all? It says nothing about which office is whose — for that you need something that checks who is asking, not just whether they are holding a key. A password on the door and nothing else is a building where everyone who got past reception can walk into every room.

Basic Auth answers only whether a caller holds the shared credentials. If different users are meant to reach different resources, the flow itself has to authorize them from a trusted identity source on top of that — and an error response must never leak which authorization check actually failed.

Every branch must respond, and must do so exactly once

Every Switch output, every bounded Catch branch, every validation-failure branch, and the normal branch all have to reach HTTP Response. Drop a branch, and the caller sits there until an upstream proxy or the client itself times out. Let two branches respond through the same msg.res, and you get a duplicate send. The safe shape is to build a fixed error envelope first, then set a controlled msg.statusCode and any header values at one single response convergence point.

flowchart TD
  S["HTTP In receives
the request"] --> V{"Validate method,
shape, and identity"} V -->|"normal"| R["HTTP Response,
one convergence point"] V -->|"missing field"| R V -->|"wrong type"| R V -->|"unauthorized"| R V -->|"internal error, from Catch"| R V -->|"timeout, from an external call"| R
Six branches, one convergence pointSix labeled arrows leave the validation diamond — normal, missing field, wrong type, unauthorized, an error caught upstream, and a timeout from an external call — and every one of them lands on the same HTTP Response box. That single box is the convergence point this chapter recommends: a status code only ever gets set in one place.

This is what such a convergence point looks like as a shape guard, kept as a pure-data illustration:

// 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;

Treat this as a shape guard only — it says nothing about identity, and it does not define a real route on its own. A status code fixed inside the HTTP Response node itself always wins. Otherwise the node reads msg.statusCode, falling back to 200 if that is not set either. For any non-Buffer object payload, it calls Express's res.jsonp(msg.payload), which means a JSONP callback name in the caller's query string can change the shape of the response envelope. If your contract requires invariant JSON, either reserve that callback query name for rejection, or serialize trusted JSON yourself and set a fixed JSON content type — both sidestep the JSONP behavior applied to plain objects.

Never let the message choose

Fix the destination in the node. Never let the message pick it.

HTTP Request 5.0.2 exposes editor fields for Method (GET, POST, PUT, DELETE, HEAD, or a value taken from msg.method), URL, GET payload handling, TLS configuration, authentication (Basic, Digest, or Bearer), keep-alive, HTTP proxy configuration, error output, the insecure HTTP parser, return type, and headers. If the node's URL field contains Mustache tags, the runtime renders them against the entire message — so a URL that looks fixed inside the node is not necessarily fixed at all. Any example here uses a complete, non-templated placeholder URL with no {{...}} in it; a real production destination needs its own, separate approval.

FieldRecommended boundaryReason
Method / URLFix the method and an approved, complete HTTPS URL in the node. No Mustache in the URL, and no untrusted msg.url or msg.methodPrevents SSRF, internal-network probing, and a write method nobody intended
PayloadIgnore the payload for GET by default. If a query or body is needed, allowlist the keys and limit their length; for other methods, fix the content type and shapeAvoids serializing and sending a whole message, or an unvalidated object
AuthenticationUse the node's own credential fields. Never embed credentials in a URL, a Function node, a header constant, or exported JSONBasic, Digest, and Bearer secrets must not enter flow source or Debug output
TLSUse a controlled TLS configuration that validates the server certificate and hostname. Never let a message disable validationWith no TLS configuration selected, msg.rejectUnauthorized can still control validation — including setting it to false
ProxyUse an HTTP Proxy configuration node and a fixed, permitted egress. Review proxy credentials and the NO_PROXY boundary tooA proxy changes the real destination, DNS behavior, and where trust actually terminates
Parser / redirectSet insecureHTTPParser=false. Disable redirects with a controlled msg.followRedirects=false, or have an egress proxy enforce destination policy insteadThe node has no redirect-host allowlist of its own; a cross-host redirect widens the SSRF surface every time it is followed
ReturnSelect only the form you actually need — text, binary buffer, or parsed JSON — and enforce a response-size limit at the egress proxy or upstream serviceThe node has no per-response byte limit; a large response just consumes memory
In plain terms

Fixing the destination in the node is a taxi with the address already programmed in before the passenger gets in. Let the message set msg.url instead, and you have handed the passenger the wheel. They can now ask the car to go anywhere the network can reach — including addresses you never meant it to visit.

Beyond its own configured fields, the runtime also reads msg.headers, msg.cookies, and msg.followRedirects, as well as msg.method, msg.url, and msg.requestTimeout when those are left dynamic. When no TLS configuration is selected, it reads msg.rejectUnauthorized too. So immediately before this node, build a fresh, allowlisted message containing only the approved payload, the required headers, and fixed control values. Never pass an HTTP In, Dashboard, or MQTT message through unchanged, and do not treat deleting one or two known fields as equivalent to rebuilding it.

flowchart LR
  M["The message arriving
at this point in the flow"] --> N["Rebuild msg from scratch:
only approved payload,
headers, and control values"] N --> O["HTTP Request node,
fixed HTTPS URL and method"] O --> P["The one approved
external service"] M -.->|"never do this"| Q["Pass msg.url or msg.method
through unchanged"] Q -.->|"caller now picks
the destination"| X["Any host the network
can reach: SSRF"]
One chain rebuilds the message, the other hands over the wheelBoth chains start at the same box. The solid chain rebuilds the message before the node and ends at the one approved service. The dotted chain is the shortcut this chapter warns against: passing the caller's own msg.url or msg.method through unchanged lets whoever sent the original message choose the destination instead of you.

If httpRequestTimeout is not configured, the core runtime's HTTP Request timeout is 120000 ms, and a positive msg.requestTimeout can override it. The implementation also permits up to 21 redirects. These are Node-RED 5.0.2 behaviors, not a policy that suits every destination — that changes between builds, so set a narrower, justified timeout for each service you actually call, and bound both the retry count and the total deadline upstream of the node. Either disable redirects outright, or require an egress proxy to enforce a destination-host allowlist and a response-size limit. Never compensate for a timeout by adding unlimited retries.

The core runtime loads the entire response into memory before doing anything else with it. 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, so checking msg.responseUrl, the content type, or the body size only after the node cannot undo an SSRF hit, a cross-host redirect, or memory exhaustion that already happened by that point. Still allowlist msg.statusCode, msg.headers, msg.responseUrl, msg.payload, msg.redirectList, and any msg.responseCookies on the successful output — understanding that this protects only how the data is used downstream, not the request that already happened. Do not pass headers or cookies unchanged into HTTP Response, and do not send the whole message to Debug.

Safe-example status. The URL in 13-outbound-http-request.json is only PLACEHOLDER_HTTPS_URL. It embeds no authentication, and HTTP Request is fixed at d:true. This is a schema you read, not a service you can connect to. Do not replace the placeholder, enable the node, or deploy it.
Answer every failure the same way

Validate in layers, and answer every failure the same way

Selecting POST in HTTP In only performs the router's own method match. Complete validation needs at least a transport layer, a syntax layer, a shape layer, an authorization layer, and a business layer. A failure at any one of them must come back as a bounded, stable error — never a stack trace, a token, an internal host name, or the raw body echoed back.

Input surfacePositive allowlistExamples to reject
MethodEach route accepts only the method its own node declaresA query field asking to switch to PUT or DELETE; retries of a non-idempotent method
Path / paramsFixed segments, with parameters constrained by character set, length, and authorized resource scope.., encoded separators, empty segments, overlong IDs, an object ID you were never authorized for
Query / bodyRequired keys, exact types, ranges, array limits, and rejection of any unknown keynull where a number was required, deeply nested objects, huge arrays, duplicate keys
HeadersA fixed content type and bounded Accept values; trust an identity header only after a controlled proxy has already overwritten and sanitized itConflicting Content-Length values, a dynamic Authorization header, forged forwarding headers
ResponseA fixed status mapping, content type, cache policy, and a minimal bodyReflected request headers or bodies, a returned exception, a Location header built from caller input

CORS is not authentication. Node-RED can add CORS middleware to HTTP nodes through the global httpNodeCors setting and handle OPTIONS accordingly — the HTTP In dialog itself has no per-route CORS field. Permit only explicit origins, methods, and headers, and never combine a permissive wildcard with credentials. Remember, too, that the browser's same-origin policy does nothing at all to stop a non-browser client.

A reverse proxy in front of any of this has to limit request-body size, header bytes, connection count, and the upstream timeout, and it must trust only the forwarding headers it overwrites itself. Write down where TLS actually terminates, whether the proxy-to-backend hop stays protected, and how the client's real IP is obtained. The Add-on's own direct TLS covers only the direct listener — Ingress and any external proxy run their own, separate TLS sessions.

A bounded error model, spelled out

StatusCodeMeaning
400INVALID_INPUTA field or type does not match the contract
401AUTH_REQUIREDAuthentication is missing or invalid
403NOT_ALLOWEDIdentity is valid, but lacks permission for this resource
404NOT_FOUNDDo not reveal any detail about a resource that is absent or simply invisible to this caller
413BODY_TOO_LARGEReject an over-limit body before parsing it
415MEDIA_TYPEAn unsupported Content-Type
429RATE_LIMITEDA bounded rate limit, with no internal counter detail exposed
500INTERNAL_ERRORA fixed, minimal body; the real detail goes only to a controlled log
504UPSTREAM_TIMEOUTThe outbound deadline expired — do not retry indefinitely
The privileged shortcut

The HA API node is an advanced privileged entry point, not a general HTTP shortcut

In HA WebSocket 0.80.3, the registered node type is ha-api. It supports either WebSocket or HTTP; the HTTP methods are GET, POST, PUT, and DELETE, and the path has its leading /api/ stripped before being handed to your existing HA server configuration. WebSocket calls must include a type. Data can be plain JSON or JSONata, and both the output location and its type are customizable. Those dynamic capabilities let one node reach a great many HA APIs from a single place — they do not mean every call it can make is read-only.

Version 0.80.3 has no Block Input Overrides option. 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, its type in msg.payload.locationType, the response type in msg.payload.responseType, and the output properties in msg.payload.outputProperties. Fixed editor fields do not form an allowlist boundary on their own. Immediately before this node, delete and rebuild the entire msg.payload, adding back only approved values — and if that is not possible, isolate the node completely from any untrusted input. Deleting only the method and the path does not block the rest of that list.
CapabilityRiskRequired restriction
Protocol / HTTP method / pathA message can change the protocol, read data, and potentially write, delete, or trigger a state changeRebuild the complete msg.payload, keeping only one approved protocol, method, and path — or isolate all untrusted input from it
WebSocket type / dataLow-level commands have broad scope, and a message can change both data and dataTypeFix the command type and its exact schema; never pass a caller's object straight through
Output controlsA message can change the output location, its type, the response type, and output properties, widening either the write or the disclosureInclude the output destination and its type in the same allowlist. Do not retain any caller-supplied output control
HA server configurationInherits the integration's connection and its token privilegesNever export the token. Keep production and test configurations separate, and apply the least privilege that still works
Debug enabled / resultsCan expose the method, the path, the data, or the HA response itselfKeep Debug disabled here. Emit only allowlisted results, and sanitize entity, device, and area IDs before they leave the node

If a Current State, Get Entities, or Action node already covers what you need, prefer that narrower node instead — the boundaries around an Action node's own target and data were covered in the earlier Action node chapter. Reach for the API node only when the case is genuinely advanced, no dedicated node exists for it, and the HA API contract is already fully established. This chapter provides no executable flow, method, path, or data for it, and does not instruct you to enable anything here.

ha-webhook is an HA event entry point, not an alias for Node-RED's HTTP In — the two do not share a lifecycle. A webhook URL or ID is itself a sensitive value and should never appear in a screenshot, a flow export, or a general log. You still need to constrain the external sources allowed to call it, guard against replay, cap the payload size, and bound whatever side effects follow. Other WebSocket, TCP, and UDP nodes each carry their own configuration and framing rules, and the paired HTTP In/HTTP Response model this chapter covers does not transfer over to them.

Fuller operational boundaries around credentials, proxies, TLS, host networking, and least privilege belong to the backup and security chapter later in this part of the guide. When timeout, memory, or connection problems come up, the performance and troubleshooting chapter after it is where to collect bounded diagnostic data — rather than reaching for full Debug output first.

When it doesn't work

The failures that actually happen

SymptomLikely causeWhat to do
HTTP In emits a message, but the caller keeps waitingA branch dropped msg.res instead of carrying it through to HTTP ResponseTrace every Switch output, every rejection branch, every Catch branch, and the normal path. Each must preserve the original msg.res. Do not extend the wait with a Delay node — validate the terminal paths on a pure-data copy first, with the route still disabled
HTTP Response reports there is no response objectThe message did not come from the same HTTP In node, or rebuilding it along the way discarded msg.resNever fabricate msg.res. Preserve the original message end to end, and overwrite only the fields you actually control
Ingress works, but the direct endpoint returns 401 or is unreachableThey are different listeners with different authentication boundariesCheck the host-port mapping, the direct TLS setup, and the http_node settings. Never enable leave_front_door_open just to debug this
The endpoint is reachable with no editor login at allDirect NGINX intentionally skips the editor's Supervisor authentication for /endpoint/Configure endpoint-specific authentication and authorization. Do not treat network location as if it were access control
HTTP Request times out, or redirects repeatedlyKeep the node disabled and check the placeholder is still in place, along with DNS, the proxy, the TLS chain, the final host, the request timeout, and the redirect policyDo not disable certificate validation, and do not configure unlimited retries to work around it
Parsed JSON succeeds at first, but later processing failsA JSON parser only guarantees syntaxAdd checks for required fields, types, ranges, depth, array length, and unknown keys — the error path still has to respond
A CORS error shows up only in a browserThe origin, the preflight method or headers, or the proxy response is not what the browser expectsNever combine a wildcard origin with credentials, and never treat a passing CORS check as authentication
The HA API node reports it “requires path/type”This is an input-contract error in an advanced nodeDo 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
Questions people ask

The ones that come up again and again

Can I use HTTP In without ever connecting it to HTTP Response?
You should not. HTTP In creates a request that is waiting for a response, and every normal, rejection, and error terminal path has to preserve that same live msg.res and send it into HTTP Response, exactly once. If the work genuinely takes a while, design an explicitly asynchronous contract instead of leaving a connection waiting with no bound at all.
If I open the editor through Home Assistant Ingress, is /endpoint automatically protected the same way?
No. Ingress, the direct editor, and direct /endpoint/ access all follow different paths. The pinned direct NGINX configuration does not run the editor's Supervisor auth_request against endpoints at all. You need http_node credentials or a controlled proxy, plus a separate authorization step of your own.
Can I put a complete URL in msg.url so one HTTP Request node can reach several services?
Not when the input is untrusted. A dynamic URL is an SSRF surface and an internal-network probe waiting to happen. Fix one approved HTTPS destination per node, or map a bounded identifier to a server-side allowlist immediately before the call. The URL itself must never contain a username, a password, a token, or any other credential.
Once CORS permits a website, is API authentication finished?
No. CORS is a browser cross-origin read policy, not authentication or authorization, and it does nothing to stop a non-browser client. You still need endpoint credentials, resource-level authorization, TLS, rate limiting, and input validation regardless of what CORS allows.
Why not replace every HA node with the HA API node?
ha-api can dynamically choose HTTP or WebSocket and its own data, which is exactly what gives it a broader privilege and input surface than a dedicated node. Dedicated nodes usually have narrower semantics and are far easier to validate. Reach for the API node only when no dedicated capability exists and the API contract is already fixed. This chapter gives you no executable call to run.
If a third party might resend the same POST, what does the offline contract need to decide first?
Decide whether the operation is safe to repeat at all, which controlled idempotency key you would use, how long that key would be kept, and what fixed status and body a duplicate request should get back. If the side effect cannot be repeated safely and there is no deduplication boundary in place, stop the design there. Neither a QoS setting, a timeout, nor an assumption that the caller “should only send it once” can fill that gap for you.
Next

Where to go from here

the contract, not the connection

You now have a written contract, two disabled example flows you've actually read, and nothing deployed

That is exactly where this chapter meant to leave you. The next chapter turns to MQTT and the optional nodes around it — a different transport, with its own broker, its own topic model, and its own version of the same question this chapter kept asking: what exactly are you trusting, and where does it end?

Open the full guide

Part 10 of the WoowTech Complete Node-RED Guide series on the Apporo blog.

Adapted from the WoowTech Complete Node-RED Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.

Light · Air · Water · Control · apporo

Reuse a flow across tabs, then stop trusting the first green light