Repository navigation
fix: only skip service requests from the client's own socket, not all of localhost - #89
chrismoorhouse wants to merge 2 commits into
Conversation
… of localhost _processServiceRequest dropped every incoming service request whose sender address was 127.0.0.1. When a BACnet router runs on the same host (e.g. bacnet-stack router-mstp bridging BACnet/IP on loopback to MS/TP), all requests it forwards - including unconfirmed COV notifications - arrive from localhost and were silently discarded, so covNotifyUnconfirmed never fired even though SubscribeCOV succeeded. Restrict the check to the client's own address and port; genuine same-port echoes are already de-duplicated by the transport layer.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The behavior fix needs automated regression coverage for same-host requests and self-echo filtering.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Fixes localhost service requests being incorrectly discarded when sent by same-host BACnet routers.
Changes:
- Filters only requests matching the client’s localhost address and port.
- Updates diagnostic logging to distinguish self-originated requests.
| File | Description |
|---|---|
src/lib/client.ts |
Narrows localhost service-request filtering to the client socket. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const ownPort = this._settings.port || DEFAULT_BACNET_PORT | ||
| const selfAddress = | ||
| LOCALHOST_INTERFACES_IPV4 + | ||
| (ownPort === DEFAULT_BACNET_PORT ? '' : `:${ownPort}`) | ||
| if (sender?.address === selfAddress) { |
robertsLando
left a comment
There was a problem hiding this comment.
Thanks, the fix is correct for the case in #88 and narrower than before. Two things before merge: regression tests and the comment wording. The rest is optional.
What the old filter actually did. The transport formats sender.address as rinfo.address plus :port only when the port isn't 47808 (src/lib/transport.ts:51-54). So the old === '127.0.0.1' check only skipped traffic from 127.0.0.1:47808, not all of localhost:
- Client on 47808: old and new filter behave the same.
- Client on another port, router on
127.0.0.1:47808: router traffic was dropped before and now gets through. This is the fix. - Client and router both on 47808 on the same host (possible since
reuseAddrdefaults to true): still dropped, and can't be told apart by address. Worth narrowing the title/description to say the fix applies when the client runs on a non-default port.
Requested: regression tests. Nothing in test/ exercises the self-skip in _processServiceRequest, so reverting to the blanket check or dropping it entirely would keep CI green. test/unit/client.spec.ts already builds clients via Object.create(BACnetClient.prototype) with a stub _settings, so the method can be called directly. Cases:
- port 47809, sender
127.0.0.1-> emitted - port 47809, sender
127.0.0.1:47809-> skipped - port 47808, sender
127.0.0.1-> skipped (no-suffix branch) - non-loopback sender -> emitted
Unchanged by this PR, for reference: own broadcast echoes come back from the LAN IP and are de-duplicated in the transport (transport.ts:33-43); the socket is udp4, so IPv6-mapped loopback never shows up.
| // Only skip requests from this client's own socket. A blanket localhost | ||
| // filter breaks topologies where a BACnet router runs on the same host | ||
| // (e.g. routed MS/TP): COV notifications and other requests forwarded by | ||
| // the router arrive from 127.0.0.1 and must be processed. True same-port | ||
| // echoes are already de-duplicated by the transport layer. |
There was a problem hiding this comment.
The last sentence overstates the transport de-dup. _lastSendMessages is only filled when send() has no receiver (transport.ts:84-96), i.e. broadcasts. A unicast to 127.0.0.1:<ownPort> is not de-duplicated there, and that's exactly the case this filter still exists for. As written, someone could read this and delete the check as redundant.
Suggest something like: "Skip unicast requests from this client's own socket. Broadcast echoes are de-duplicated by the transport layer."
| // (e.g. routed MS/TP): COV notifications and other requests forwarded by | ||
| // the router arrive from 127.0.0.1 and must be processed. True same-port | ||
| // echoes are already de-duplicated by the transport layer. | ||
| const ownPort = this._settings.port || DEFAULT_BACNET_PORT |
There was a problem hiding this comment.
Two small points (both optional):
|| DEFAULT_BACNET_PORTnever applies: the constructor already sets the default (client.ts:215).- "Own" comes from
_settings.port, not the socket actually bound. With a customoptions.transportbound to, say, 47810,_settings.portstays 47808, so a router on 47808 is dropped again while real self-traffic from:47810isn't. Not a regression (same as before), but the transport already emits its real address onlistening(transport.ts:58-62) andclient.ts:239discards it. Could be a follow-up.
| const selfAddress = | ||
| LOCALHOST_INTERFACES_IPV4 + | ||
| (ownPort === DEFAULT_BACNET_PORT ? '' : `:${ownPort}`) |
There was a problem hiding this comment.
Nit / follow-up: this is the third inline copy of the "omit :port when it's 47808" rule (also transport.ts:51-54 and bvlc.ts:83-85). If the transport format ever changes, selfAddress stops matching and self-requests silently stop being filtered. A shared formatter would keep them in sync.

Summary
Fixes #88.
_processServiceRequest()drops every incoming service request whose sender address is127.0.0.1. When a BACnet router or device runs on the same host (e.g. bacnet-stackrouter-mstpbridging BACnet/IP on loopback to MS/TP), everything it forwards — including unconfirmed COV notifications — arrives from localhost and is silently discarded, so events likecovNotifyUnconfirmednever fire even though the SubscribeCOV itself is acknowledged.Change
Restrict the filter to the client's own address and port (
127.0.0.1+:portsuffix for non-default ports, matching the transport's sender formatting). Genuine same-port self-echoes are already de-duplicated byte-for-byte by the transport layer (_lastSendMessages), so the original intent of the check is preserved.Verification
npm run buildcleannpm run test:unit— 204/204 pass127.0.0.1:47809, bacnet-stackrouter-mstponlo:47808 ↔ MS/TP): before this change COV notifications were dropped withReceived and skipped localhost service request; after it,covNotifyUnconfirmedfires and notifications flow.