Skip to content

HCR081

Dispose streams returned from HTTP content.

Why

Streams returned by HttpClient.GetStreamAsync(...), HttpContent.ReadAsStreamAsync(...), and synchronous HttpContent.ReadAsStream() hold response/content resources until they are disposed or ownership is clearly handed to another component. Leaving those streams undisposed can keep buffers, sockets, or file handles alive longer than intended.

Bad

public async Task CopyAsync(
    HttpResponseMessage response,
    Stream destination,
    CancellationToken cancellationToken)
{
    var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
    await stream.CopyToAsync(destination, cancellationToken);
}

Better

public async Task CopyAsync(
    HttpResponseMessage response,
    Stream destination,
    CancellationToken cancellationToken)
{
    using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
    await stream.CopyToAsync(destination, cancellationToken);
}

Returning the stream is also acceptable when the caller owns it:

public async Task<Stream> OpenAsync(
    HttpClient client,
    CancellationToken cancellationToken)
{
    var stream = await client.GetStreamAsync("https://example.com", cancellationToken);
    return stream;
}

Current Detection

The implementation reports local declarations and simple assignments where a stream is materialized from HttpClient.GetStreamAsync(...), HttpContent.ReadAsStreamAsync(...), or synchronous HttpContent.ReadAsStream() and the local stream is not visibly disposed or transferred.

It recognizes using var, using (stream), await using var, await using (stream), direct Dispose() and DisposeAsync(), disposal in finally, returning the stream, and returning an object that is visibly constructed or initialized with the stream. Disposal, using ownership, direct returns, and returned-owner construction can flow through visible local alias chains populated by initializers or top-level standalone assignments. Parentheses and null-forgiving operators are transparent around stream acquisition, aliases, disposal, and direct ownership transfers. Locals, aliases, and returned owner locals are reassignment-aware, resolved custom HttpClient lookalikes are skipped, and resolved stream methods must belong to the BCL HttpClient or HttpContent types rather than unrelated custom extensions that reuse the same names.

The code fix converts a simple single-variable local declaration to using var. It also merges an immediately adjacent uninitialized declaration and assignment into one using declaration. Existing using and await using declarations are left unchanged. Non-adjacent or nested assignments are not changed because their ownership scope requires broader control-flow judgment.

Cancellation-token flow into streaming APIs is covered by HCR064; this rule focuses on stream ownership.

Suppression

Suppress only when stream ownership is transferred through a pattern the analyzer cannot currently see, such as storing it in a long-lived owner that is disposed elsewhere.

References