Skip to content

[BUG] GET /faults/stream on an aggregating gateway returns 200 and streams nothing #611

Description

@bburda

Bug report

Steps to reproduce

  1. 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.
  2. Open GET /api/v1/faults/stream on the aggregator and on a peer that has live faults, in the
    same time window.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions