From 4c9ebd4d2fc81e76375672d54233ae7b3e0debd9 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Tue, 28 Jul 2026 08:53:57 -0500 Subject: [PATCH] ProductSupport#34: document and cover the broken HTTP-transport control channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This sample's battery only ever exercised telemetry (service → console). The control direction (console → service) has never worked on the pinned Wolverine, and nothing here noticed — which is how a reporter followed this sample closely and hit a wall. The console's comment was actively misleading. It said the named HttpClient for a service's control URL is "created on demand from the service's reported control URI". Nothing creates it. WolverineHttpTransportClient uses the outbound URI purely as an IHttpClientFactory client NAME and posts to that client's BaseAddress; CreateClient returns a default client with a null BaseAddress for an unknown name, so the send throws "An invalid request URI was provided…". The console cannot pre-register the client either — it only learns a service's control URL at runtime from that service's own registration. Fixed upstream in wolverine#3681 (GH-3690). Here: the comment and README now say what actually happens and what to delete after the bump, and there is a test that would have caught it. The test asserts on the console's own logs rather than driving an operator command, because operator commands travel CritterWatch's SignalR hub and this harness has no SignalR client — but it still pins the defect, since the console pushes to a registered service unprompted and a broken send shows up with no operator action at all. It is [Fact(Skip)] because it legitimately fails on the pinned WolverineFx 6.23.1. Un-skip with the bump; both the README and the skip reason say so. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01U6rL8dGQMMh9xNYtP9R9jP --- .../CritterWatchConsole/Program.cs | 15 ++++++-- critterwatch/WebService.Http/README.md | 13 +++++++ .../Tests/WebServiceHttpSmokeTests.cs | 38 +++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/critterwatch/WebService.Http/CritterWatchConsole/Program.cs b/critterwatch/WebService.Http/CritterWatchConsole/Program.cs index 1c29451..0912c0f 100644 --- a/critterwatch/WebService.Http/CritterWatchConsole/Program.cs +++ b/critterwatch/WebService.Http/CritterWatchConsole/Program.cs @@ -52,9 +52,18 @@ builder.Services.AddWolverineHttp(); // The console can also push operator commands (PauseProjection / RebuildProjection / DLQ ops / …) BACK to -// the OrderService over the same HTTP transport. Sending over the transport needs the transport client; -// register it so those sends work when an operator triggers them. (The named HttpClient that targets a -// specific service's control URL is created on demand from the service's reported control URI.) +// the OrderService over the same HTTP transport. Sending over the transport needs the transport client. +// +// ⚠️ On WolverineFx 6.23.1 (the pin below) this registration is NOT sufficient, and the control channel +// does not work. An earlier version of this comment claimed the named HttpClient for a service's control +// URL is "created on demand from the service's reported control URI" — it is not, and nothing creates it. +// WolverineHttpTransportClient uses the outbound URI purely as an IHttpClientFactory client NAME and then +// posts to that client's BaseAddress; CreateClient returns a default client with a null BaseAddress for an +// unknown name, so the send throws "An invalid request URI was provided…". The console cannot pre-register +// a named client either, because it only learns a service's control URL at runtime from that service's own +// registration. Reported as ProductSupport#34, fixed in wolverine#3681 (GH-3690): the transport falls back +// to its own isolated client and posts to the absolute outbound URI, and AddWolverineHttp() registers both +// of these lines for you. DELETE THEM when this sample bumps past that Wolverine release. builder.Services.AddScoped(); builder.Services.AddHttpClient(); diff --git a/critterwatch/WebService.Http/README.md b/critterwatch/WebService.Http/README.md index 5f65731..1bf335f 100644 --- a/critterwatch/WebService.Http/README.md +++ b/critterwatch/WebService.Http/README.md @@ -44,6 +44,19 @@ receiver's HTTP endpoint.) `IHttpClientFactory.CreateClient(outboundUri)` and POSTs to `client.BaseAddress`). - `MapWolverineHttpTransportEndpoints()` so the console can POST operator commands **back** to this service. +> ⚠️ **The control channel (console → service) does not work on WolverineFx 6.23.1.** Telemetry +> (service → console) is fine. The send side resolves its `HttpClient` by using the outbound URI as an +> `IHttpClientFactory` client *name* and then posts to that client's `BaseAddress`; for a name nobody +> registered, `CreateClient` returns a default client whose `BaseAddress` is `null`, and the send throws +> `"An invalid request URI was provided. Either the request URI must be an absolute URI or BaseAddress +> must be set."` The console **cannot** work around it — it only learns a service's control URL at runtime +> from that service's own registration, so there is no point at which it could register a named client. +> +> Reported as ProductSupport#34, fixed in wolverine#3681 (GH-3690). When this sample bumps past that +> release: delete the two manual registration lines in `CritterWatchConsole/Program.cs` +> (`AddWolverineHttp()` registers them), and un-skip +> `WebServiceHttpSmokeTests.console_control_channel_sends_without_transport_errors`. + **CritterWatchConsole (receiver)** — `CritterWatchConsole/Program.cs`: - `AddCritterWatch(...)` for the dashboard/store, then `app.MapWolverineHttpTransportEndpoints()` to receive envelopes at `/_wolverine/invoke`, executed inline against the console's CritterWatch handlers. diff --git a/critterwatch/WebService.Http/Tests/WebServiceHttpSmokeTests.cs b/critterwatch/WebService.Http/Tests/WebServiceHttpSmokeTests.cs index bf46017..6a39e22 100644 --- a/critterwatch/WebService.Http/Tests/WebServiceHttpSmokeTests.cs +++ b/critterwatch/WebService.Http/Tests/WebServiceHttpSmokeTests.cs @@ -76,4 +76,42 @@ public async Task order_service_registers_with_the_console_over_http_transport() services.Select(s => s.Id).ShouldContain("OrderService"); } + + /// + /// The CONTROL direction of the monitoring link — console → service — which this battery never + /// covered. Everything above exercises telemetry (service → console), which is why ProductSupport#34 + /// shipped: the console's send side throws on every attempt, and nothing here noticed. + /// + /// + /// + /// Asserted through the console's logs rather than by driving an operator command, because operator + /// commands travel CritterWatch's SignalR hub and this harness has no SignalR client. It still pins + /// the defect: the console pushes to a registered service on its own, so a broken send surfaces as + /// BufferedSendingAgent failures in the console log with no operator action at all — exactly + /// how the reporter found it. + /// + /// + /// SKIPPED until this sample bumps past wolverine#3681 (GH-3690). It fails on the pinned WolverineFx + /// 6.23.1 — legitimately, because the control channel really is broken there. Un-skip with the bump; + /// see the warning in README.md. + /// + /// + [Fact(Skip = "Control channel is broken on WolverineFx 6.23.1 — un-skip when this sample bumps past wolverine#3681 (GH-3690).")] + public async Task console_control_channel_sends_without_transport_errors() + { + using var client = _fixture.CreateCritterWatchClient(); + + // Registration first: the console has nothing to send to until OrderService reports its control URI. + await _fixture.WaitForServicesAsync(client, ["OrderService"], timeout: TimeSpan.FromMinutes(2)); + + // Give the console a window to actually push to the freshly-registered service. + await Task.Delay(TimeSpan.FromSeconds(20)); + + var consoleLogs = _fixture.DumpResourceLogs(CritterWatchAppHostFixture.CritterWatchResourceName); + + // The GH-3690 signature. Matched on the exception text rather than the sender type so the assertion + // survives Wolverine renaming BufferedSendingAgent. + consoleLogs.ShouldNotContain("An invalid request URI was provided"); + consoleLogs.ShouldNotContain("Failed to send outgoing envelopes batch"); + } }