{"record":{"id":"476b76a16e139c2f","repo":"microsoft/aspire","slug":"cancellation-token-must-be-cancellable-in-order-to-prevent","errorCode":null,"errorMessage":"Cancellation token must be cancellable in order to prevent leaking resources when following logs.","messagePattern":"Cancellation token must be cancellable in order to prevent leaking resources when following logs\\.","errorType":"exception","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Hosting/Dcp/ResourceLogSource.cs","lineNumber":40,"sourceCode":"\ninternal sealed class ResourceLogSource<TResource>(\n    ILogger logger,\n    IKubernetesService kubernetesService,\n    TResource resource,\n    bool follow) :\n    IAsyncEnumerable<IReadOnlyList<ResourceLogEntry>>\n    where TResource : CustomResource, IKubernetesStaticMetadata\n{\n    public async IAsyncEnumerator<IReadOnlyList<ResourceLogEntry>> GetAsyncEnumerator(CancellationToken cancellationToken)\n    {\n        // For follow mode, we require a cancellable token to stop streaming.\n        // For non-follow mode (snapshot), streams complete naturally so we create our own cancellable token if needed.\n        CancellationTokenSource? ownedCts = null;\n        if (!cancellationToken.CanBeCanceled)\n        {\n            if (follow)\n            {\n                throw new ArgumentException(\"Cancellation token must be cancellable in order to prevent leaking resources when following logs.\", nameof(cancellationToken));\n            }\n            // Create our own cancellable token for the APIs that require it.\n            // For non-follow mode, streams complete naturally when all logs are read.\n            ownedCts = new CancellationTokenSource();\n            cancellationToken = ownedCts.Token;\n        }\n\n        var channel = Channel.CreateUnbounded<ResourceLogEntry>(new UnboundedChannelOptions\n        {\n            AllowSynchronousContinuations = false,\n            SingleReader = true,\n            SingleWriter = false\n        });\n\n        async Task StreamLogsAsync(Stream stream, bool isError, bool parseDcpLogs)\n        {\n            try\n            {","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Hosting/Dcp/ResourceLogSource.cs#L22-L58","documentation":"ResourceLogSource streams logs from DCP resources. In follow mode the stream never completes on its own, so its resources are only cleaned up when the caller's cancellation token fires; a token that cannot be canceled (CancellationToken.None) would leak the stream. GetAsyncEnumerator therefore throws ArgumentException when follow=true and the token is non-cancellable.","triggerScenarios":"Calling a log-streaming API (Watch/Stream logs) with follow: true and CancellationToken.None, or a default CancellationToken parameter that never gets a real token.","commonSituations":"Calling WatchLogs from a console host without a linked token, forgetting to pass an application-stopping token, or a library wrapper that forwards CancellationToken.None.","solutions":["Pass a cancellable token, e.g. CancellationTokenSource.CreateLinkedTokenSource(applicationStopping).Token.","Create a CancellationTokenSource whose token you pass to the enumerator.","If you only want a snapshot, pass follow: false — a non-cancellable token is then allowed."],"exampleFix":"// before\nawait foreach (var log in logSource.WatchLogs(follow: true, CancellationToken.None)) { }\n// after\nusing var cts = new CancellationTokenSource();\nawait foreach (var log in logSource.WatchLogs(follow: true, cts.Token)) { }","handlingStrategy":"validation","validationCode":"if (follow && !cancellationToken.CanBeCanceled)\n    throw new ArgumentException(\"follow requires a cancellable token.\", nameof(cancellationToken));","typeGuard":null,"tryCatchPattern":"try { await foreach (var line in source.WatchLogs(follow: true, token)) { ... } }\ncatch (ArgumentException ex) when (ex.ParamName == nameof(cancellationToken)) { /* pass a real CTS token */ }","preventionTips":["Always thread the host's ApplicationStopping token into log watchers","Never pass CancellationToken.None to follow-mode streams","Default parameters to a linked CTS rather than default(CancellationToken)"],"tags":["logs","streaming","cancellation","resource-leak"],"backgroundTag":"missing-required-argument","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T09:17:21.228Z"}