Every HttpClient Names Its Pipeline

A Polly line that reads Source: '-standard//Standard-AttemptTimeout' does not mean "a caller resolved an unnamed HttpClient". It means "a client that rides the portal's shared defaults pipeline" — which, until #4528, was every client except the two plugin-registry ones, and all of them shared ONE pipeline instance.

Why the slot was empty

ServiceDefaults.AddServiceDefaults turns resilience on for every client through ConfigureHttpClientDefaults(http => http.AddStandardResilienceHandler(…)). Two facts of Microsoft.Extensions.Http.Resilience (read from the 10.10.0 assembly the portal ships) make the rest follow:

Fact Consequence
AddStandardResilienceHandler names its pipeline PipelineNameHelper.GetName(builder.Name, "standard") = {builder.Name}-standard the builder ConfigureHttpClientDefaults hands out has no name, so the pipeline is -standard for every client it configures — named or not
the pipeline registry keys a pipeline by HttpKey(name, instance), and without a selector the instance is "" every such client, and every host they call, got the same pipeline — one retry budget, one circuit breaker — and every line read -standard//…

So self-update-handover, deployment-report, module-published-broadcast, OpenGraphPreview, the OCI tag listers, and every Plugins client that registers by name (WebSearchPlugin, TeamsClient, the Apple/Google/Chess clients…) all logged as the same nobody. #1133/#1137 had fixed the symptom for the registry clients by re-registering them with RemoveAllResilienceHandlers() — which is why plugin-registry-standard//… always named itself, and why the fix below leaves those two alone.

What it actually was (measured)

Read-only Logs InstanceActions on memex.meshweaver.cloud, 2026-09-24:

The fix: the instance is the client's name

ServiceDefaults.AddHttpClientResilienceDefaults (called by AddServiceDefaults, and by the test that pins it) now:

  1. puts an outermost DelegatingHandler on every client that writes HttpMessageHandlerBuilder.Name into request.Options (ClientNameKey) — registered through ConfigureAll<HttpClientFactoryOptions>, where the name is known, and inserted at index 0 so it runs before any pipeline is selected;
  2. adds .SelectPipelineBy(_ => ClientNameOf) to the defaults' standard handler, so the pipeline instance is that name ((unnamed) for the factory's default client).

Every line now reads -standard/self-update-handover/Standard-AttemptTimeout, and each client trips its own breaker instead of one shared by all of them.

🚨 Keyed by client NAME, never by SelectPipelineByAuthority. The defaults also serve arbitrary URLs taken from user data — an agent's web fetch, an Open Graph preview — and the pipeline registry keeps every instance it ever built, so a per-host key would grow with whatever hosts users paste. Client names are fixed in code; the set is bounded.

EveryClientNamesItsOwnResiliencePipelineTest (Memex.Portal.Shared.Test) builds exactly what production builds and reads the identity off the Polly log line itself. Its negative control — dropping the SelectPipelineBy call — reproduces the production form, '-standard//Standard-Retry'.

What this does NOT fix