Bug report
Steps to reproduce
- Run an aggregator gateway with one or more peers. Each peer runs on its own ROS domain with
its own fault_manager. Give the aggregator its own domain and no local fault producers.
- Open
GET /api/v1/faults/stream on the aggregator and on a peer that has live faults, in the
same time window.
- Trigger faults on the peer.
Expected behavior
The aggregator relays the fault events of its peers, the same way GET /api/v1/faults already
fans out to peers. If it cannot do that, it should not advertise a route it cannot serve, and
should return an explicit error instead.
Actual behavior
The aggregator returns 200 OK with Content-Type: text/event-stream and then sends only
:keepalive comments. Measured against the owning peer in the same 60 s window:
aggregator -> 0 events, 1 ":keepalive"
owning peer -> 10 events
Repeated in a separate 20 s window: aggregator 0 events, owning peer 84 events.
A 200 with an open but silent stream looks exactly like "there are no faults". A client that
trusts it will report the system as healthy. In a deployment where the aggregator is the only
port an operator can reach, this is the only fault stream available.
Environment
- ros2_medkit version: main (a268379), gateway vendor_info.version 0.6.0, SOVD API 1.0.0
- ROS 2 distro: Jazzy
- OS: Ubuntu 24.04
Additional information
SSEFaultHandler only subscribes to its own ROS domain. The constructor at
src/ros2_medkit_gateway/src/http/handlers/sse_fault_handler.cpp:68 creates one subscription to
<namespace>/fault_manager/events. The file contains no reference to aggregat, peer or
fan_out. An aggregating gateway runs on its own domain, with its own fault_manager that no
producer reports to.
GET /faults does fan out (src/http/handlers/fault_handlers.cpp:449). The stream does not.
SSEStreamProxy already exists and looks complete. It is declared at
include/ros2_medkit_gateway/core/aggregation/stream_proxy.hpp:81, implemented in
src/core/aggregation/sse_stream_proxy.cpp, and compiled into gateway_core. It is never
constructed outside test/test_stream_proxy.cpp. The symbol has no other reference in
production code, and the file history is two commits, both moves and Rule-of-Five boilerplate.
The route is registered unconditionally (src/http/rest_server.cpp:1564), declared with
.response(200, ...) (src/core/openapi/route_registry.cpp:266), and listed as a public read
path in all three auth presets (src/core/auth/auth_config.cpp:108,252,466). The only non-200
path in the handler is the SSE client limit 503.
Two documents disagree about this:
src/ros2_medkit_gateway/design/aggregation.rst:537-542 says SSEStreamProxy "connects to a
peer's SSE endpoint and relays events back to the primary gateway's client". No code does
this.
docs/api/rest.rst:2760-2765 lists the endpoints that fan out and correctly leaves
/faults/stream out.
Whichever way this is fixed, the design document should stop describing behaviour that does not
exist.
Bug report
Steps to reproduce
its own
fault_manager. Give the aggregator its own domain and no local fault producers.GET /api/v1/faults/streamon the aggregator and on a peer that has live faults, in thesame time window.
Expected behavior
The aggregator relays the fault events of its peers, the same way
GET /api/v1/faultsalreadyfans out to peers. If it cannot do that, it should not advertise a route it cannot serve, and
should return an explicit error instead.
Actual behavior
The aggregator returns
200 OKwithContent-Type: text/event-streamand then sends only:keepalivecomments. Measured against the owning peer in the same 60 s window:Repeated in a separate 20 s window: aggregator 0 events, owning peer 84 events.
A 200 with an open but silent stream looks exactly like "there are no faults". A client that
trusts it will report the system as healthy. In a deployment where the aggregator is the only
port an operator can reach, this is the only fault stream available.
Environment
Additional information
SSEFaultHandleronly subscribes to its own ROS domain. The constructor atsrc/ros2_medkit_gateway/src/http/handlers/sse_fault_handler.cpp:68creates one subscription to<namespace>/fault_manager/events. The file contains no reference toaggregat,peerorfan_out. An aggregating gateway runs on its own domain, with its ownfault_managerthat noproducer reports to.
GET /faultsdoes fan out (src/http/handlers/fault_handlers.cpp:449). The stream does not.SSEStreamProxyalready exists and looks complete. It is declared atinclude/ros2_medkit_gateway/core/aggregation/stream_proxy.hpp:81, implemented insrc/core/aggregation/sse_stream_proxy.cpp, and compiled intogateway_core. It is neverconstructed outside
test/test_stream_proxy.cpp. The symbol has no other reference inproduction code, and the file history is two commits, both moves and Rule-of-Five boilerplate.
The route is registered unconditionally (
src/http/rest_server.cpp:1564), declared with.response(200, ...)(src/core/openapi/route_registry.cpp:266), and listed as a public readpath in all three auth presets (
src/core/auth/auth_config.cpp:108,252,466). The only non-200path in the handler is the SSE client limit 503.
Two documents disagree about this:
src/ros2_medkit_gateway/design/aggregation.rst:537-542saysSSEStreamProxy"connects to apeer's SSE endpoint and relays events back to the primary gateway's client". No code does
this.
docs/api/rest.rst:2760-2765lists the endpoints that fan out and correctly leaves/faults/streamout.Whichever way this is fixed, the design document should stop describing behaviour that does not
exist.