Skip to content

Issue SignalR token validation on its own hub, not the mesh router - #1066

Merged
rbuergi merged 6 commits into
mainfrom
fix/signalr-token-validation-off-router
Aug 10, 2026
Merged

rbuergi merged 6 commits into
mainfrom
fix/signalr-token-validation-off-router

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The defect

MeshWeaver.Messaging.MessageHub logs ROUTER_TRAFFIC at ERROR ~2,385 times per 24 h across the
three production portals — the single largest source of red log lines. The detector fires when the
ROOT MESH HUB (the mesh's router) is an end of a delivery.

SignalRConnectionRegistry.Authenticate issued its per-connection ValidateTokenRequest on the
DI-injected IMessageHub — which is the root mesh/{id} hub. That makes the router an end of the
delivery in both directions:

  • the request reaches ApiToken/{hashPrefix} stamped Sender = mesh/{id} →
    RawJson has the mesh hub as sender (sender: mesh/…, target: ApiToken/…)
  • its response is addressed straight back at the router →
    ValidateTokenResponse has the mesh hub as target (sender: ApiToken/…, target: mesh/…)

Both shapes are in the production logs verbatim. GrpcConnectionRegistry had already been fixed (its
TransportHub, with a 🚨 doc block explaining exactly this); SignalR had not.

The fix

SignalRConnectionRegistry gets the same TransportHub — a stable portal/signalr-{instance} hub,
created lazily, with the RegisterStream registration Orleans needs for the response to land
cross-silo (portal is a stream-routed address type). One line changes at the call site; the rest is
the hub property and its rationale.

The test

RouterTrafficTest captures the detector's own ERROR output and asserts the mesh hub never
originates a delivery.

🚨 The assertion is on the sender role only, deliberately. "Mesh hub as SENDER" is unambiguous —
the router actually originated that message. "Mesh hub as TARGET" is reported by
MessageHub.DeliverMessage for the receiving hub, so it also fires for a pure routing hop:
HierarchicalRouting.RouteAlongHostingHierarchy:218 hands every delivery a hosted hub cannot route
locally to parentHub.DeliverMessage, and for every hub in the process that parent is the mesh hub.
Asserting on those would pin an invariant the architecture does not hold — see the note below.

Verified to bite: with the fix reverted the test fails with

The mesh hub is the ROUTER and must never ORIGINATE a delivery — work belongs on a hub of its own.
Offending ROUTER_TRAFFIC lines:
ROUTER_TRAFFIC: RawJson has the mesh hub as sender (sender: mesh/WwgCB8mQQUiUrPusB6Sp1w, target: ApiToken/43c387d784f4).

What this does NOT fix — and what I learned about the rest

A deterministic repro (MonolithMeshTestBase, capturing every ROUTER_TRAFFIC line) showed the
remaining lines split into two groups:

  1. Routing hops — ValidateTokenRequest/ValidateTokenResponse/SubscribeRequest/
    DataChangedEvent/SubscribeAck reported as "mesh hub as target". These come from
    HierarchicalRouting.RouteAlongHostingHierarchy:218, not from any application caller. They cannot
    be removed: that hop is what gives RoutingServiceBase.RouteInMesh its single-threaded
    arrival-order guarantee for per-address activation. The detector is untouched — no level
    change, no predicate change, no mute.
  2. Node CRUD on the router — MeshExtensions.NodeOperationTarget falls back to
    hub.GetMeshHub().Address for every hub with no WithNodeOperationExecution ancestor, i.e. every
    per-node hub. This is the genuine, high-volume half (the production
    RawJson has the mesh hub as sender (… target: …/Source/FNodeTypeAtomicSolution) line, reported
    once per per-node hub). It is deliberately not in this PR — see the review notes for why it
    needs its own cycle.

rbuergi and others added 4 commits August 10, 2026 06:40
ROUTER_TRAFFIC ERROR lines in production trace to the ROOT MESH HUB being an
END of a delivery, not a hop:

- MeshExtensions.NodeOperationTarget fell back to hub.GetMeshHub().Address for
  every hub with no WithNodeOperationExecution ancestor — i.e. every per-node
  hub. The create ran on the router's action block AND its response came back
  stamped Sender = mesh/{id} (the prod "RawJson has the mesh hub as sender"
  line, reported once per per-node hub). It now falls back to a dedicated
  portal/nodeops-{meshId} execution hub.
- SignalRConnectionRegistry issued its ValidateTokenRequest on the DI-injected
  IMessageHub, which IS the root mesh hub. It now uses its own transport portal
  hub, mirroring GrpcConnectionRegistry.TransportHub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A hosted hub's MessageHubConfiguration starts EMPTY (CreateMessageHub builds a
fresh one; nothing is inherited from the parent), and ResolveEvaluator does not
walk the parent chain — it is
`hub.Configuration.Get<EffectivePermissionsDelegate>() ?? DefaultEvaluator`,
and the default returns Permission.All. So the new nodeops execution hub granted
every write until it copied the mesh hub's evaluator.

Caught by OwnerlessPartitionRepairTest: a stranger's create into an ownerless
partition succeeded where it must be refused.

Also re-gates EnsurePartitionBootstrap on "is the central node-operation hub"
instead of "is the mesh hub" — the bootstrap runs where the create executes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SignalRConnectionRegistry.Authenticate posted its per-connection
ValidateTokenRequest on the DI-injected IMessageHub — which IS the root
mesh/{id} hub, the mesh's ROUTER. That makes the router an END of the delivery
in both directions: the request reaches ApiToken/{hashPrefix} stamped
Sender = mesh/{id}, and the response is addressed straight back at mesh/{id}.
Both are what the ROUTER_TRAFFIC detector reports, and in production they were
part of the largest single source of ERROR lines (~2,385/24h across the three
portals).

GrpcConnectionRegistry already had the fix (its TransportHub); SignalR is now
wired the same way — a stable portal/signalr-{instance} hub with the
RegisterStream registration Orleans needs for the response to land cross-silo.

RouterTrafficTest pins it by capturing the detector's own output and asserting
the mesh hub never ORIGINATES a delivery. The assertion is on the sender role
only, deliberately: "mesh hub as target" also fires for a pure routing hop
(HierarchicalRouting.RouteAlongHostingHierarchy:218 hands every non-local
delivery to parentHub.DeliverMessage, and that parent is the mesh hub for every
hub in the process), and those hops are the design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 10, 2026 05:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prevents SignalR token validation from originating on the root mesh/{id} hub (the mesh router) by issuing the ValidateTokenRequest from a dedicated, stream-registered transport portal hub instead. This aligns SignalR with the existing gRPC transport pattern and reduces ROUTER_TRAFFIC ERROR noise caused by the router becoming an end-of-delivery.

Changes:

  • Introduces a lazily-created TransportHub (portal/signalr-{instance}) in SignalRConnectionRegistry and uses it to issue ValidateTokenRequest.
  • Adds an integration-style regression test (RouterTrafficTest) that captures ROUTER_TRAFFIC logs and asserts the router never sends such deliveries.
  • Adds the corresponding “What’s New” entry documenting the user-visible impact.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
test/MeshWeaver.Hosting.Monolith.Test/RouterTrafficTest.cs Adds a regression test that fails if the mesh router originates ROUTER_TRAFFIC during SignalR authentication.
test/MeshWeaver.Hosting.Monolith.Test/MeshWeaver.Hosting.Monolith.Test.csproj Adds a project reference needed to compile/run the new SignalR-focused test.
src/MeshWeaver.Hosting.SignalR/SignalRConnectionRegistry.cs Routes token validation through a dedicated, stream-registered transport hub rather than the root mesh hub.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-09-token-checks-no-longer-run-on-the-router.md Documents the behavioral change as a fix in the changelog.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

697 tests  ±0   697 ✅ +1   14m 46s ⏱️ + 1m 19s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌  - 1 

Results for commit 0651681. ± Comparison against base commit bd250ef.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 755 tests  +1 755   1 563 ✅ +1 563   5m 4s ⏱️ + 5m 4s
   11 suites +   11     192 💤 +  192 
   11 files   +   11       0 ❌ ±    0 

Results for commit 0651681. ± Comparison against base commit bd250ef.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 195 tests   - 26   1 195 ✅  - 26   6m 21s ⏱️ +48s
   11 suites ± 0       0 💤 ± 0 
   11 files   ± 0       0 ❌ ± 0 

Results for commit 0651681. ± Comparison against base commit bd250ef.

This pull request removes 67 and adds 41 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.SearchExclusionIntegrationTest ‑ NoSearchContext_StillReturns_UnderscorePrefixedPaths
MeshWeaver.Hosting.Monolith.Test.SearchExclusionIntegrationTest ‑ Search_Context_Excludes_UnderscorePrefixedPaths
MeshWeaver.Hosting.Monolith.Test.SearchResultStormTest ‑ CatalogSubscription_ContentUpdateStorm_AddsNoExtraRerenders
MeshWeaver.Hosting.Monolith.Test.SendDocumentToContactsTest ‑ NoAttachmentSendEmail_StillWorks
MeshWeaver.Hosting.Monolith.Test.SendDocumentToContactsTest ‑ SendDeckToContacts_EmailsPdfAttachment_ToUserAndRawAddress
MeshWeaver.Hosting.Monolith.Test.SlideLayoutAreaTest ‑ ContentArea_FirstFrame_CarriesDeckPosition
MeshWeaver.Hosting.Monolith.Test.SlideLayoutAreaTest ‑ ContentArea_MiddleSlide_NavigatesOrderedSiblings
MeshWeaver.Hosting.Monolith.Test.SlideLayoutAreaTest ‑ PresentArea_IsChromeFree_WithClickableStage
MeshWeaver.Hosting.Monolith.Test.SlideLayoutAreaTest ‑ SlideContent_RoundTrips_WithShortTypeDiscriminator
MeshWeaver.Hosting.Monolith.Test.SpaceEditMarkdownTest ‑ SpaceEdit_MarkdownBackedSpace_LoadsContentIntoStandardEditor
…
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ CreateNode_PreservesName
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ Project_CanBeResolved
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ Project_PingRequest_ShouldNotDeadlock
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ Project_QueryAsync_ScopeDescendants_FindsCodeNodes
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ Todo_QueryAsync_FindsCodeNodes
MeshWeaver.Hosting.Monolith.Test.SearchResultAccumulatorTest ‑ ContentUpdateStorm_TriggersNoRerender_OnlyStructuralChangesDo
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ Failed_availability_write_never_blocks_the_roll_forward
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ MidLife_fault_reestablishes_silently_and_never_reapplies_the_default_policy
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ Poller_survives_faulted_first_read_and_processes_a_policy_change
MeshWeaver.Hosting.Monolith.Test.ShutdownRoutingRejectClassificationTest ‑ RoutingRejectWhileMeshIsShuttingDown_IsTransientShuttingDown_NotTerminalFailed
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 284 tests  ±0   1 279 ✅ ±0   6m 1s ⏱️ +33s
   11 suites ±0       5 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 0651681. ± Comparison against base commit bd250ef.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 968 tests  +27   1 966 ✅ +27   6m 45s ⏱️ +21s
   11 suites ± 0       2 💤 ± 0 
   11 files   ± 0       0 ❌ ± 0 

Results for commit 0651681. ± Comparison against base commit bd250ef.

This pull request removes 41 and adds 68 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ CreateNode_PreservesName
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ Project_CanBeResolved
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ Project_PingRequest_ShouldNotDeadlock
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ Project_QueryAsync_ScopeDescendants_FindsCodeNodes
MeshWeaver.Hosting.Monolith.Test.SamplesGraphDataTest ‑ Todo_QueryAsync_FindsCodeNodes
MeshWeaver.Hosting.Monolith.Test.SearchResultAccumulatorTest ‑ ContentUpdateStorm_TriggersNoRerender_OnlyStructuralChangesDo
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ Failed_availability_write_never_blocks_the_roll_forward
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ MidLife_fault_reestablishes_silently_and_never_reapplies_the_default_policy
MeshWeaver.Hosting.Monolith.Test.SelfUpdatePollerResilienceTest ‑ Poller_survives_faulted_first_read_and_processes_a_policy_change
MeshWeaver.Hosting.Monolith.Test.ShutdownRoutingRejectClassificationTest ‑ RoutingRejectWhileMeshIsShuttingDown_IsTransientShuttingDown_NotTerminalFailed
…
MeshWeaver.Hosting.Monolith.Test.RouterTrafficTest ‑ SignalRTokenValidation_IsNeverIssuedByTheRouter
MeshWeaver.Hosting.Monolith.Test.SearchExclusionIntegrationTest ‑ NoSearchContext_StillReturns_UnderscorePrefixedPaths
MeshWeaver.Hosting.Monolith.Test.SearchExclusionIntegrationTest ‑ Search_Context_Excludes_UnderscorePrefixedPaths
MeshWeaver.Hosting.Monolith.Test.SearchResultStormTest ‑ CatalogSubscription_ContentUpdateStorm_AddsNoExtraRerenders
MeshWeaver.Hosting.Monolith.Test.SendDocumentToContactsTest ‑ NoAttachmentSendEmail_StillWorks
MeshWeaver.Hosting.Monolith.Test.SendDocumentToContactsTest ‑ SendDeckToContacts_EmailsPdfAttachment_ToUserAndRawAddress
MeshWeaver.Hosting.Monolith.Test.SlideLayoutAreaTest ‑ ContentArea_FirstFrame_CarriesDeckPosition
MeshWeaver.Hosting.Monolith.Test.SlideLayoutAreaTest ‑ ContentArea_MiddleSlide_NavigatesOrderedSiblings
MeshWeaver.Hosting.Monolith.Test.SlideLayoutAreaTest ‑ PresentArea_IsChromeFree_WithClickableStage
MeshWeaver.Hosting.Monolith.Test.SlideLayoutAreaTest ‑ SlideContent_RoundTrips_WithShortTypeDiscriminator
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   6m 6s ⏱️ +35s
1 977 tests ±0  1 869 ✅ ±0  108 💤 ±0  0 ❌ ±0 
2 129 runs  +1  2 021 ✅ +1  108 💤 ±0  0 ❌ ±0 

Results for commit 0651681. ± Comparison against base commit bd250ef.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   65 files  +   11     65 suites  +11   45m 6s ⏱️ + 8m 41s
8 876 tests +1 756  8 569 ✅ +1 565  307 💤 +192  0 ❌  - 1 
9 028 runs  +1 757  8 721 ✅ +1 566  307 💤 +192  0 ❌  - 1 

Results for commit 0651681. ± Comparison against base commit bd250ef.

♻️ This comment has been updated with latest results.

@rbuergi
rbuergi merged commit df1ed76 into main Aug 10, 2026
17 checks passed
@rbuergi
rbuergi deleted the fix/signalr-token-validation-off-router branch August 19, 2026 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants