The bug
The per-indexer catch in IndexerSearchWorkflow.SearchIndexersAsync excluded OperationCanceledException from its filter. An HttpClient per-request timeout throws TaskCanceledException — a subclass of OperationCanceledException — so a single slow or timing-out indexer escaped the per-indexer handling, propagated out of Task.WhenAll, and discarded every healthy indexer's results for the whole search. On the automatic path that zeroes out the entire cycle for that book until the next sweep.
The fix
Catch OperationCanceledException per-indexer, log a warning naming the indexer, and return an empty list for that indexer only — matching how other per-indexer failures already behave. No workflow-level CancellationToken flows into this search (verified across the call chain, incl. IIndexerSearchProvider.SearchAsync), so an OCE here can only be a client timeout; there is no shutdown cancellation to preserve. OutOfMemoryException/StackOverflowException remain excluded.
Tests
SearchIndexersAsync_OneIndexerTimesOut_ReturnsHealthyIndexerResultsSearchIndexersAsync_AllIndexersTimeOut_ReturnsEmptyWithoutThrowing
Full suite green: 1191 passed / 0 failed; dotnet format --verify-no-changes clean.
🤖 Generated with Claude Code
Automated Canary build