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.
apiMaxLength is left unset. A parser limit, not a sensible limit for your routehttpRequestTimeout is not configured — two minutes, unless a positive msg.requestTimeout overrides itStart 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.
| Capability | Data direction | Main risks | This chapter's approach |
|---|---|---|---|
| HTTP In → HTTP Response | A network request enters a flow, which then answers the caller | Unauthorized access, oversized bodies, path or header injection, a branch that never responds | Fixed method and path, allowlist validation, a response on every branch, and the route disabled until it is reviewed |
| HTTP Request | A flow sends a request to an external HTTPS service | SSRF, credential disclosure, redirects, an unbounded response, and no built-in timeout | Fixed destination, credentials kept in the node's own fields, a bounded timeout, placeholders only in examples |
| HA API node | Calls an HTTP or WebSocket API through an existing HA server configuration | Reads or changes Home Assistant with the integration's own privileges; dynamic method, path, and data widen that further | Reserved for an advanced, approved, allowlisted case — this chapter creates and enables nothing |
| Webhook, WebSocket, TCP, UDP | Long-lived connections or other protocols entirely | Each has its own authentication, source, message-size, and lifecycle rules | Do 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.
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"]
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.
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.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.
| Field | Source or purpose | Security interpretation |
|---|---|---|
msg.req | The live request; exposes headers, params, query, cookies, and more | Treat all of it as untrusted. Project out only allowlisted fields, and do not retain the object itself |
msg.res | The live response wrapper, carried along the original path to HTTP Response | Preserve it on every success, rejection, and error branch. Respond through it exactly once |
msg.payload | The GET query, or the body under another supported method | Successful parsing proves nothing about shape — still check type, length, range, and unknown fields |
msg.statusCode | A dynamic status code, when HTTP Response has no fixed status of its own | Generate it only from a controlled mapping. Never trust caller input directly |
msg.headers / msg.cookies | HTTP Response can merge these into the response headers and cookies | Allowlist 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 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.
-
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_PATHfor the path — never a real host or a real secret. -
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.reqormsg.resin a Delay node, in context, or on a path that waits on an external event with no bound. -
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 carryd: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. -
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 onhttp_node.username/passwordor a controlled reverse proxy. Remember that this Basic Auth mechanism is not fine-grained authorization — it is next. -
Step 5
Review the outbound design
Read the paired offline example,
13-outbound-http-request.json. Confirm its URL isPLACEHOLDER_HTTPS_URL, that HTTP Request carriesd:true, that there are no authentication, TLS, or proxy references anywhere in it, and that Inject will not fire at startup. -
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_METHODpath: /PLACEHOLDER_HTTP_PATHrequest content type: PLACEHOLDER_MEDIA_TYPErequest max bytes: PLACEHOLDER_LIMITsuccess: PLACEHOLDER_STATUS + fixed shapereject: PLACEHOLDER_STATUS + no internal detailsexternal I/O: do not execute/PLACEHOLDER_HTTP_PATH, a written byte limit, and a defined response for all six branches from Step 2.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 a timeout | Any field is still a guess, a real host or secret appears in the document, or answering it requires actually sending a request |
| Terminal-path 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 to verify |
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 point | Pinned-source behavior | Authentication and TLS boundary |
|---|---|---|
| Ingress | The 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 backend | The Supervisor path handles it. That does not mean a direct port or a custom route gets the same protection |
| Direct listener | The container's 80/tcp can map to host port 1880; whether it is actually exposed depends on your Supervisor port settings | ssl, 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 path | Do 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 entirely | Protect it yourself with Add-on http_node Basic Auth or a controlled proxy — authorization is still a separate step |
| Static content | Served by Node-RED's own static route | http_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"]
/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.
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
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.resconst 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.
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.
| Field | Recommended boundary | Reason |
|---|---|---|
| Method / URL | Fix the method and an approved, complete HTTPS URL in the node. No Mustache in the URL, and no untrusted msg.url or msg.method | Prevents SSRF, internal-network probing, and a write method nobody intended |
| Payload | Ignore 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 shape | Avoids serializing and sending a whole message, or an unvalidated object |
| Authentication | Use the node's own credential fields. Never embed credentials in a URL, a Function node, a 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 | With no TLS configuration selected, msg.rejectUnauthorized can still control validation — including setting it to false |
| Proxy | Use an HTTP Proxy configuration node and a fixed, permitted egress. Review proxy credentials and the NO_PROXY boundary too | A proxy changes the real destination, DNS behavior, and where trust actually terminates |
| Parser / redirect | Set insecureHTTPParser=false. Disable redirects with a controlled msg.followRedirects=false, or have an egress proxy enforce destination policy instead | The node has no redirect-host allowlist of its own; a cross-host redirect widens the SSRF surface every time it is followed |
| Return | Select only the form you actually need — text, binary buffer, or parsed JSON — and enforce a response-size limit at the egress proxy or upstream service | The node has no per-response byte limit; a large response just consumes memory |
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"]
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.
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.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 surface | Positive allowlist | Examples to reject |
|---|---|---|
| Method | Each route accepts only the method its own node declares | A query field asking to switch to PUT or DELETE; retries of a non-idempotent method |
| Path / params | Fixed 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 / body | Required keys, exact types, ranges, array limits, and rejection of any unknown key | null where a number was required, deeply nested objects, huge arrays, duplicate keys |
| Headers | A fixed content type and bounded Accept values; trust an identity header only after a controlled proxy has already overwritten and sanitized it | Conflicting Content-Length values, a dynamic Authorization header, forged forwarding headers |
| Response | A fixed status mapping, content type, cache policy, and a minimal body | Reflected 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
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_INPUT | A 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 any detail about a resource that is absent or simply invisible to this caller |
| 413 | BODY_TOO_LARGE | Reject an over-limit body before parsing it |
| 415 | MEDIA_TYPE | An unsupported Content-Type |
| 429 | RATE_LIMITED | A bounded rate limit, with no internal counter detail exposed |
| 500 | INTERNAL_ERROR | A fixed, minimal body; the real detail goes only to a controlled log |
| 504 | UPSTREAM_TIMEOUT | The outbound deadline expired — do not retry indefinitely |
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.
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.| 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, keeping only one approved protocol, method, and path — or isolate all untrusted input from it |
WebSocket type / data | Low-level commands have broad scope, and a message can change both data and dataType | Fix the command type and its exact schema; never pass a caller's object straight through |
| Output controls | A message can change the output location, its type, the response type, and output properties, widening either the write or the disclosure | Include the output destination and its type in the same allowlist. Do not retain any caller-supplied output control |
| HA server configuration | Inherits the integration's connection and its token privileges | Never export the token. Keep production and test configurations separate, and apply the least privilege that still works |
| Debug enabled / results | Can expose the method, the path, the data, or the HA response itself | Keep 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.
The failures that actually happen
| Symptom | Likely cause | What to do |
|---|---|---|
| HTTP In emits a message, but the caller keeps waiting | A branch dropped msg.res instead of carrying it through to HTTP Response | Trace 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 object | The message did not come from the same HTTP In node, or rebuilding it along the way discarded msg.res | Never 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 unreachable | They are different listeners with different authentication boundaries | Check 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 all | Direct 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 repeatedly | Keep 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 policy | Do not disable certificate validation, and do not configure unlimited retries to work around it |
| Parsed JSON succeeds at first, but later processing fails | A JSON parser only guarantees syntax | Add 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 browser | The origin, the preflight method or headers, or the proxy response is not what the browser expects | Never 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 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 |
The ones that come up again and again
Can I use HTTP In without ever connecting it to HTTP Response?
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?
/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?
Once CORS permits a website, is API authentication finished?
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?
Where to go from here
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 guidePart 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