Skip to content

Latest commit

 

History

History
75 lines (57 loc) · 3.14 KB

File metadata and controls

75 lines (57 loc) · 3.14 KB
title HCR065 — Do not send the same HttpRequestMessage more than once
description An HttpRequestMessage can only be sent once; a second SendAsync throws InvalidOperationException.

HCR065

Do not send the same HttpRequestMessage more than once.

Why

HttpClient.SendAsync marks the request message as sent. Sending the same instance again throws InvalidOperationException ("The request message was already sent") — a guaranteed crash. This is the canonical bug inside hand-rolled DelegatingHandler retry loops that call base.SendAsync(request, ...) on every iteration, and it also appears when a request local is reused across two sends in one method.

Bad

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken)
{
    for (var attempt = 0; attempt < 3; attempt++)
    {
        var response = await base.SendAsync(request, cancellationToken);
        if (response.IsSuccessStatusCode)
        {
            return response;
        }
    }

    return response;
}
var request = new HttpRequestMessage(HttpMethod.Post, "/orders");
await client.SendAsync(request, cancellationToken);
await client.SendAsync(request, cancellationToken); // throws

Better

Clone the request per attempt — the standard resilience handlers do exactly this:

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken)
{
    for (var attempt = 0; attempt < 3; attempt++)
    {
        using var attemptRequest = await CloneRequestAsync(request, cancellationToken);
        var response = await base.SendAsync(attemptRequest, cancellationToken);
        if (response.IsSuccessStatusCode)
        {
            return response;
        }
    }
}

Or prefer AddStandardResilienceHandler/AddResilienceHandler, which already implement retry with request cloning.

Current Detection

The implementation reports a second Send/SendAsync on the same HttpRequestMessage local or parameter within one block when the variable is not reassigned between the sends, and a send inside a loop when the request variable is not assigned a fresh instance inside the loop body. Receivers are validated with Roslyn type information (HttpClient, HttpMessageInvoker, HttpMessageHandler, DelegatingHandler), with a syntactic fallback for unresolved base.SendAsync overrides and visibly HttpClient-typed receivers. Reassigned variables, distinct request instances, and convenience methods (GetAsync, PostAsync, …) that build their own request are skipped.

Suppression

Suppress only when the second send is provably unreachable (for example inside a branch that always returns first). Prefer cloning or a resilience handler over suppression.

References