Repository navigation
Description
What is failing
The webhook inbox delivery endpoint throws an unhandled ObjectDisposedException when it registers a response subject on a portal MessageHub that has already begun shutting down. The caller of the webhook receives a generic 500 instead of a graceful, retry-able response.
Probable cause
A shutdown race, high confidence: the exception message is explicit — "Hub portal/nodeops-… is shutting down — cannot register new response subject". WebhookInboxEndpoints.Deliver obtains the IMessageHub and uses it without guarding against the hub being disposed mid-request (or mid-pod-drain). The ObjectDisposedException escapes the endpoint and is caught only by ExceptionHandlerMiddleware. Fix shape: catch ObjectDisposedException (or check hub lifetime) in Deliver and return 503 Service Unavailable with Retry-After, so webhook senders retry against another pod instead of treating the delivery as permanently failed.
Impact
Four occurrences on a single portal pod within one hour, all against the same hub (portal/nodeops-…) — consistent with webhook deliveries arriving while that hub was being torn down / the pod drained. Low volume, but each hit is a webhook delivery (likely an external integration) that failed with a misleading 500.
Where to look
memex/Memex.Portal.Shared/Api/WebhookInboxEndpoints.cs, WebhookInboxEndpoints.Deliver (stack points at line 87), and the MessageHub response-subject registration it calls. The source is not present as a node in this mesh; start from the repo checkout. Related shutdown-race pattern: other ObjectDisposedException incidents on the MessageHub (Admin/_LogIncident/4e86cd5956ddddaf) and HostedHubsCollection (Admin/_LogIncident/8bd7c9c44c12e40b), but none covers this specific log site — not a duplicate.
Evidence
| Fingerprint | 379d85c4215afb7c |
| Category | Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware |
| Severity | Error |
| Exception | System.ObjectDisposedException |
| Top frame | Memex.Portal.Shared.Api.WebhookInboxEndpoints.Deliver(String target, HttpRequest request, IMessageHub hub, IConfiguration config, CancellationToken ct) |
| Namespace | memex |
| Pods | memex-portal-deployment-5998d5fddf-9sfsv |
| Occurrences | 4 |
| First seen | 2026-09-21 09:01:22Z |
| Last seen | 2026-09-21 09:56:24Z |
| Routing | not determined — no configured route matches the category Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware. This repository is the configured fallback, not a finding about who owns the fault; the category names the LOGGER, which may not be the subject. |
Recent log lines
2026-09-21 09:01:22Z memex-portal-deployment-5998d5fddf-9sfsv fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.ObjectDisposedException: Hub portal/nodeops-wpWb1fSJPUacoVxY6LnltQ is shutting down — cannot register new response subject for 0QkLhFDkrEeU3bGILeNc4Q.
Object name: 'MessageHub'.
at Memex.Portal.Shared.Api.WebhookInboxEndpoints.Deliver(String target, HttpRequest request, IMessageHub hub, IConfiguration config, CancellationToken ct) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Api/WebhookInboxEndpoints.cs:line 87
at Microsoft.AspNetCore.Http.RequestDelegateFactory.ExecuteTaskResult[T](Task`1 task, HttpContext httpContext)
at Memex.Portal.Shared.Authentication.OnboardingMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Authentication/OnboardingMiddleware.cs:line 166
at Memex.Portal.Shared.Authentication.VirtualUserMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Authentication/VirtualUserMiddleware.cs:line 78
at MeshWeaver.Hosting.AspNetCore.Portal.UserContextMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/src/MeshWeaver.Hosting.AspNetCore/Portal/UserContextMiddleware.cs:line 277
at Memex.Portal.Shared.Seo.PublicSite.<>c__DisplayClass7_0.<<UsePublicHostRedirect>b__0>d.MoveNext() in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Seo/PublicSite.cs:line 121
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at MeshWeaver.Hosting.AspNetCore.MeshModuleStaticAssetExtensions.<>c__DisplayClass6_0.<<UseMeshModuleStaticAssets>b__1>d.MoveNext() in /home/ru…[truncated]
2026-09-21 09:49:24Z memex-portal-deployment-5998d5fddf-9sfsv fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.ObjectDisposedException: Hub portal/nodeops-wpWb1fSJPUacoVxY6LnltQ is shutting down — cannot register new response subject for R72RlgvmX0mkG4ECK1dl7A.
Object name: 'MessageHub'.
at Memex.Portal.Shared.Api.WebhookInboxEndpoints.Deliver(String target, HttpRequest request, IMessageHub hub, IConfiguration config, CancellationToken ct) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Api/WebhookInboxEndpoints.cs:line 87
at Microsoft.AspNetCore.Http.RequestDelegateFactory.ExecuteTaskResult[T](Task`1 task, HttpContext httpContext)
at Memex.Portal.Shared.Authentication.OnboardingMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Authentication/OnboardingMiddleware.cs:line 166
at Memex.Portal.Shared.Authentication.VirtualUserMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Authentication/VirtualUserMiddleware.cs:line 78
at MeshWeaver.Hosting.AspNetCore.Portal.UserContextMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/src/MeshWeaver.Hosting.AspNetCore/Portal/UserContextMiddleware.cs:line 277
at Memex.Portal.Shared.Seo.PublicSite.<>c__DisplayClass7_0.<<UsePublicHostRedirect>b__0>d.MoveNext() in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Seo/PublicSite.cs:line 121
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at MeshWeaver.Hosting.AspNetCore.MeshModuleStaticAssetExtensions.<>c__DisplayClass6_0.<<UseMeshModuleStaticAssets>b__1>d.MoveNext() in /home/ru…[truncated]
2026-09-21 09:56:24Z memex-portal-deployment-5998d5fddf-9sfsv fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.ObjectDisposedException: Hub portal/nodeops-wpWb1fSJPUacoVxY6LnltQ is shutting down — cannot register new response subject for pbxE-hzl3kKLKsVYeShjGw.
Object name: 'MessageHub'.
at Memex.Portal.Shared.Api.WebhookInboxEndpoints.Deliver(String target, HttpRequest request, IMessageHub hub, IConfiguration config, CancellationToken ct) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Api/WebhookInboxEndpoints.cs:line 87
at Microsoft.AspNetCore.Http.RequestDelegateFactory.ExecuteTaskResult[T](Task`1 task, HttpContext httpContext)
at Memex.Portal.Shared.Authentication.OnboardingMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Authentication/OnboardingMiddleware.cs:line 166
at Memex.Portal.Shared.Authentication.VirtualUserMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Authentication/VirtualUserMiddleware.cs:line 78
at MeshWeaver.Hosting.AspNetCore.Portal.UserContextMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/src/MeshWeaver.Hosting.AspNetCore/Portal/UserContextMiddleware.cs:line 277
at Memex.Portal.Shared.Seo.PublicSite.<>c__DisplayClass7_0.<<UsePublicHostRedirect>b__0>d.MoveNext() in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Seo/PublicSite.cs:line 121
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at MeshWeaver.Hosting.AspNetCore.MeshModuleStaticAssetExtensions.<>c__DisplayClass6_0.<<UseMeshModuleStaticAssets>b__1>d.MoveNext() in /home/ru…[truncated]
2026-09-21 09:56:24Z memex-portal-deployment-5998d5fddf-9sfsv fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.ObjectDisposedException: Hub portal/nodeops-wpWb1fSJPUacoVxY6LnltQ is shutting down — cannot register new response subject for jKO3a6DXqUSr0BW1VSFY5A.
Object name: 'MessageHub'.
at Memex.Portal.Shared.Api.WebhookInboxEndpoints.Deliver(String target, HttpRequest request, IMessageHub hub, IConfiguration config, CancellationToken ct) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Api/WebhookInboxEndpoints.cs:line 87
at Microsoft.AspNetCore.Http.RequestDelegateFactory.ExecuteTaskResult[T](Task`1 task, HttpContext httpContext)
at Memex.Portal.Shared.Authentication.OnboardingMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Authentication/OnboardingMiddleware.cs:line 166
at Memex.Portal.Shared.Authentication.VirtualUserMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Authentication/VirtualUserMiddleware.cs:line 78
at MeshWeaver.Hosting.AspNetCore.Portal.UserContextMiddleware.InvokeAsync(HttpContext context) in /home/runner/work/MeshWeaver/MeshWeaver/src/MeshWeaver.Hosting.AspNetCore/Portal/UserContextMiddleware.cs:line 277
at Memex.Portal.Shared.Seo.PublicSite.<>c__DisplayClass7_0.<<UsePublicHostRedirect>b__0>d.MoveNext() in /home/runner/work/MeshWeaver/MeshWeaver/memex/Memex.Portal.Shared/Seo/PublicSite.cs:line 121
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at MeshWeaver.Hosting.AspNetCore.MeshModuleStaticAssetExtensions.<>c__DisplayClass6_0.<<UseMeshModuleStaticAssets>b__1>d.MoveNext() in /home/ru…[truncated]
Opened automatically from Admin/_LogIncident/379d85c4215afb7c. Recurrences are folded into this issue rather than opening new ones.
Activity
systemorph commentedon Sep 21, 2026
Still occurring: 1 more since the last update (5 total).
memex-portal-deployment-5998d5fddf-9sfsvRecent log lines
rbuergi commentedon Sep 21, 2026
Cluster triage: this symptom has five distinct roots
Reviewed together with #1540 #1547 #1548 #3243 #3593 #2833 #2480 #5064 #4859 #5136 #4847 #4883 #4888 #4961 — every one of them reports
ObjectDisposedExceptionwithAutofac.Core.Lifetime.LifetimeScope.ThrowDisposedException()on top, or a teardown that did not finish. They are not one bug. The grouping, the discriminators and the evidence are committed asDoc/Architecture/DisposedScopeAndDyingHubs(added in #5159).Root R2 — identified, and deliberately NOT fixed yet. The two obvious fixes are both unsafe today.
The mechanism is confirmed in code.
HostedHubsCollection.GetHubWithOutcomeanswers a registry hitHostedHubOutcome.Availableunconditionally, while a hosted hub leaves that registry only through aRegisterForDisposalcallback that runs insideMessageHub.DisposeImpl— several phases afterDispose()setIsDisposing, and after the hub already refuses new work (GetOrAddResponseSubjectanswers exactly the
ObjectDisposedExceptionin this report fromRunLevel >= ShutDown). Between thosetwo points the lookup reports a hub that can serve nothing.
The window is not bounded, which is what makes your evidence read the way it does: a teardown that
wedges never reaches the callback at all (#4883 measured one
sync/hub holding its own ShutDown turnfor 96+ seconds and still counting). Four deliveries to the same
portal/nodeops-…identity across 55minutes on a pod that was otherwise serving is not a pod drain — the Kubernetes grace ceiling ends
those — it is one hub that stopped working and was never evicted. The caller reaches it through
NodeOperationExecutionHub, i.e.GetHostedHub(…, HostedHubCreation.Always).Why I did not ship either fix:
null+HostShuttingDown) reads correct and is not. 38 call sites usethe two-argument
GetHostedHub, which is documented to return non-null forAlwaysand isdereferenced accordingly — several assign it straight into a non-nullable field. Refusing converts an
attributable
ObjectDisposedExceptioninto an unattributableNullReferenceExceptionin code thatworks today, on the everyday recycle path.
answer and needs three removals to be value-conditional first, so a predecessor's teardown cannot
erase its successor. Two are addressed; the third is the blocker:
MonolithRoutingServiceandOrleansRoutingService; fixed in fix(mesh): resolve while the scope is alive — the delete pipeline's own continuations (#5064) #5159 (a registration now removes what itregistered,
subscriptionReadyincluded);IPodHubGrain.Detachstamps a terminalReleasedtombstone on the address for ten minutes,which the router reads as
NotFound+TargetUnserved— the verdict that triggers owner-sidestream eviction — and a successor's
Attachclears it only if it runs afterwards. So apredecessor's
Detachlanding after a successor'sAttachwould get the live hub evicted as acorpse. Strictly worse than the 500 this issue reports. That is a grain-contract change with a
cross-repo surface and needs its own scope call.
🚨 And a lead that may make all of the above moot, filed into triage rather than asserted here:
HostedHubsCollection.Add— the only method that registers both the disposal-time registry removaland
CloseScopeWhenDisposed— appears unreferenced insrc/. The one insertion path in use, theper-address
Lazyfactory inGetHubWithOutcome, assigns the entry directly and does neither. If thatis real rather than a call site my sweep missed, a hosted hub never leaves the registry at all and
its scope is never closed — a simpler and stronger root for this issue than the phase window above, plus
a per-hub scope leak. Not established: it contradicts the documented recycle flow and the
scope-closing tests, so verify before building on it.
Full write-up, including the three prerequisites and the discriminators that separate this from the
other four roots:
Doc/Architecture/DisposedScopeAndDyingHubs.rbuergi commentedon Sep 21, 2026
Correction to my comment above, measured rather than estimated: the count of call sites that would begin receiving
nullif the lookup refused a dying hub is 25, not 38. The larger figure came from a grep that also counted comments, doc mentions and the declarations themselves.The argument is unchanged — 25 sites consume the null-forgiving two-argument
GetHostedHub— and it is now specific instead of summarised: six of them assign the result straight into a non-nullable field or return it from a method with a non-nullable return type (Activity,PortalApplication,SessionHubFactory,PartitionStorageRouter,MeshNodeStreamCache,StaticRepoImporter). Those are where refusal would turn an attributableObjectDisposedExceptioninto an unattributableNullReferenceException.Also aligned: this issue's evidence is four occurrences at three distinct times (09:01, 09:49, 09:56) spanning 55 minutes; the doc page said "three deliveries" and now matches the evidence table here. Corrected in
Doc/Architecture/DisposedScopeAndDyingHubsby commitb978623488on #5159.rbuergi commentedon Sep 22, 2026
Fixed at the lookup, and the three removals are now all value-matched — #5199.
The two obvious fixes this thread ruled out stay ruled out. What landed is the third one the page named, once its prerequisites were in:
HostedHubsCollection.GetHubWithOutcomeno longer answers a registered hub atRunLevel >= ShutDownto anAlwayscaller. It retires it (value-matched removal, so a successor a concurrent lookup registered is untouched), mints a successor through the ordinary single-flight creation, and keeps the corpse in the collection's disposal join until its ownDisposalCompleted— its scope is a child of the owner's, and the owner must not close it under a registrant walk still running. OneWarningline,[HOSTED-RETIRE], names the address and phase; the ordinary path never reaches it.ShutDown, notIsDisposing, on purpose: belowShutDownthe hub is still draining accepted work and the intake's transientShuttingDownNACK is the documented answer; a successor minted there would overlap accepted writes on the first activation. AtShutDownnothing accepted remains — the measured shape here (cannot register new response subject) is exactly that phase.PodHubGrain.Detachleaves the claim alone when the silo still holds a live local route for the address —RegisterStream's disposal removes its route before it releases the claim, so a live route at that point can only be a successor's. No grain-contract change. Two more key-only removals found in the sweep are gone too:MonolithRoutingService.UnregisterStream(address)(armed on every route-created hub, in the same registrant walk) and thepodHubClaimSettledseam.Controls, measured (xUnit v3 host run directly; the hub dll's mtime confirmed after the source's on each side):
AHubAtShutDownIsNotHandedOutTest(parks the ShutDown turn inside a reactive dispose action, asserts the corpse is still registered)PodHubStaleReleaseTest(real two-silo cluster)Detach: total 2, failed 1 — the successor's delivery threwPodHubNotHereException … was RELEASED by its owner; the honest-release case passed on both sides, as it mustWhat this does not fix: the wedge itself. A hub parked at
ShutDownfor 55 minutes is #4883's shape; this lets callers past it and names it, it does not free the turn. That stays on #4883.Closing on the merge: the fix is compiled core code, it ships in the next image, and this thread carries no post-fix recurrence. Reopen if a
cannot register new response subjectfault from a hosted-hub address recurs on an image carrying this commit without a[HOSTED-RETIRE]line for that address preceding it — that would mean a path that hands out a corpse this lookup does not see.fix(compile): resolve the settle's survivor per use, never hold it (r…
fix(orleans): the release-in-flight marker is a set of per-registrati…