The content pipeline reports cancellation as a return value rather than an exception at several layers, so callers can't distinguish a cancelled shutdown from a genuine failure.
#357 / #358 fixed this at the two ends — ContentOrchestrator rethrows for the caller's token, and the three profile reconcilers check the token after every orchestrator call. The layers in between were left alone as out of scope for an alpha-4 backport.
Not reachable today — do this with the UI cancellation work, not before
Every current entry point into the pipeline passes CancellationToken.None or omits the token:
| Caller |
Token |
ContentBrowserViewModel.SearchAsync |
none |
PublisherCardViewModel → AcquireContentAsync |
none |
PublisherCardViewModel → ProfileContentService |
explicit CancellationToken.None (×2) |
GameProfileLauncherViewModel → PublisherProfileOrchestrator |
none (×4) |
ProfileLauncherFacade.LaunchProfileAsync |
none (both callers) |
ProfileContentService and PublisherProfileOrchestrator do thread tokens down to the orchestrator, but their own callers hand them None. PublisherCardViewModel has a real _cts cancelled on dispose, but it only feeds _profileManager — it never reaches the orchestrator.
The only genuinely cancellable path into the pipeline is the reconcilers on shutdown, which #357/#358 already fixed. So no current code path can produce the state described here.
This matters for sequencing. The expensive part isn't adding rethrows — it's deciding, per awaited dependency, whether a failure observed under a cancelled token is cancellation or a real error. With no reachable scenario, that's a guess. Whoever wires real cancellation into the UI (cancel button on downloads, shutdown propagation through browse/acquire) should do this sweep alongside it, so the semantics are chosen against behaviour that can actually be tested.
Remaining sites
BaseContentProvider.PrepareContentAsync — bare catch (Exception) converts cancellation to a failed OperationResult
SuperHackersProvider.PrepareContentInternalAsync — same, and runs before the base class, so fixing only BaseContentProvider changes nothing for this provider
CommunityOutpostProvider / GeneralsOnlineProvider / GitHubContentProvider / LocalFileSystemContentProvider — same shape
FileSystemDeliverer, HttpContentDeliverer
ContentOrchestrator.RemoveContentAsync — off the reconciler path, same pattern
ContentOrchestrator.AcquireContentAsync / EnsureInstallationPoolPathAsync — dependencies returning a failure value after cancellation are still treated as real errors
One trap worth knowing
Filter on the caller's token. HttpClient timeouts surface as TaskCanceledException, which derives from OperationCanceledException, so an unconditional rethrow lets one slow provider abort an entire aggregate search — this was caught in review on #358 after exactly that mistake. Use catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested).
The content pipeline reports cancellation as a return value rather than an exception at several layers, so callers can't distinguish a cancelled shutdown from a genuine failure.
#357 / #358 fixed this at the two ends —
ContentOrchestratorrethrows for the caller's token, and the three profile reconcilers check the token after every orchestrator call. The layers in between were left alone as out of scope for an alpha-4 backport.Not reachable today — do this with the UI cancellation work, not before
Every current entry point into the pipeline passes
CancellationToken.Noneor omits the token:ContentBrowserViewModel.SearchAsyncPublisherCardViewModel→AcquireContentAsyncPublisherCardViewModel→ProfileContentServiceCancellationToken.None(×2)GameProfileLauncherViewModel→PublisherProfileOrchestratorProfileLauncherFacade.LaunchProfileAsyncProfileContentServiceandPublisherProfileOrchestratordo thread tokens down to the orchestrator, but their own callers hand themNone.PublisherCardViewModelhas a real_ctscancelled on dispose, but it only feeds_profileManager— it never reaches the orchestrator.The only genuinely cancellable path into the pipeline is the reconcilers on shutdown, which #357/#358 already fixed. So no current code path can produce the state described here.
This matters for sequencing. The expensive part isn't adding rethrows — it's deciding, per awaited dependency, whether a failure observed under a cancelled token is cancellation or a real error. With no reachable scenario, that's a guess. Whoever wires real cancellation into the UI (cancel button on downloads, shutdown propagation through browse/acquire) should do this sweep alongside it, so the semantics are chosen against behaviour that can actually be tested.
Remaining sites
BaseContentProvider.PrepareContentAsync— barecatch (Exception)converts cancellation to a failedOperationResultSuperHackersProvider.PrepareContentInternalAsync— same, and runs before the base class, so fixing onlyBaseContentProviderchanges nothing for this providerCommunityOutpostProvider/GeneralsOnlineProvider/GitHubContentProvider/LocalFileSystemContentProvider— same shapeFileSystemDeliverer,HttpContentDelivererContentOrchestrator.RemoveContentAsync— off the reconciler path, same patternContentOrchestrator.AcquireContentAsync/EnsureInstallationPoolPathAsync— dependencies returning a failure value after cancellation are still treated as real errorsOne trap worth knowing
Filter on the caller's token.
HttpClienttimeouts surface asTaskCanceledException, which derives fromOperationCanceledException, so an unconditional rethrow lets one slow provider abort an entire aggregate search — this was caught in review on #358 after exactly that mistake. Usecatch (OperationCanceledException) when (cancellationToken.IsCancellationRequested).