Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.InboundAPI/IStandbyRequestForwarder.cs
T
Joseph Doherty a9b23556eb feat(inbound-api): standby central node relays POST /api/* to the active peer instead of 503
A caller addressing a standby central node directly (or hitting Traefik inside its
/health/active probe window after a failover) used to get 503 STANDBY_NODE. The
standby now relays the request verbatim to the active peer over HTTP
(IStandbyRequestForwarder / HttpStandbyRequestForwarder) and replays the response;
method scripts and Route.To() still execute only on the active node.

- ScadaBridge:InboundApi:StandbyForwardPeerEndpoints (static list of the OTHER
  central node's HTTP base URL; empty = old 503 behaviour) + StandbyForwardTimeout
  (60s), both startup-validated.
- Peers tried in order: peer 503 stamped X-ScadaBridge-Standby, connection failure
  or timeout → next peer; all fail → 503 ACTIVE_NODE_UNREACHABLE. A 503 without
  the marker is the active node's own answer and is replayed.
- One-hop loop guard via X-ScadaBridge-Forwarded; 413 body cap now runs BEFORE
  the active-node check so a standby never buffers/relays an oversized body.
- AuditWriteMiddleware skips its own ApiInbound row for a relayed request so a
  call is audited once, on the node that executed it.
- Rig + env2 central nodes configured; spec (Component-InboundAPI.md → Standby
  forwarding), Component-Host.md options row, CLAUDE.md updated.
- 20 new tests (filter decision + relay mechanics + validator).

Claude-Session: https://claude.ai/code/session_019ZxXT5sxZs5XrtdwYzJDPV
2026-08-28 13:34:20 -04:00

34 lines
1.7 KiB
C#

using Microsoft.AspNetCore.Http;
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
/// <summary>
/// Standby-node forwarding seam for <c>POST /api/{methodName}</c>. When a request
/// lands on a standby central node, <see cref="InboundApiEndpointFilter"/> hands it
/// here instead of returning HTTP 503, and the implementation relays it verbatim to
/// the active central node over HTTP so callers that address a central node directly
/// (or that hit the load balancer inside its health-probe window after a failover)
/// still get a served response. Method scripts and <c>Route.To()</c> calls still run
/// ONLY on the active node — the standby is a pure relay.
/// </summary>
public interface IStandbyRequestForwarder
{
/// <summary>
/// <c>true</c> when at least one peer endpoint is configured
/// (<see cref="InboundApiOptions.StandbyForwardPeerEndpoints"/>). When <c>false</c>
/// the filter falls back to the pre-forwarding behaviour: HTTP 503 <c>STANDBY_NODE</c>.
/// </summary>
bool IsConfigured { get; }
/// <summary>
/// Relays the current request to the first peer that serves it and returns a
/// result that replays the peer's status, content type and body to the caller.
/// Never throws for peer failures: when no peer served the request the result is
/// HTTP 503 <c>ACTIVE_NODE_UNREACHABLE</c>.
/// </summary>
/// <param name="context">The inbound HTTP context on the standby node.</param>
/// <param name="cancellationToken">Cancelled when the caller aborts the request.</param>
/// <returns>A result replaying the peer's response, or a 503 when no peer served it.</returns>
Task<IResult> ForwardAsync(HttpContext context, CancellationToken cancellationToken);
}