Define the operation before choosing a retry policy
A retry repeats an operation; it is not simply an exception-handling decoration. The sample chooses a GET request, one specifically named transient status and a maximum of three attempts. Its injected RetryHandler returns 503 on the first call and 200 on the next. Those controlled results let the program illustrate its decision policy without a live service, a clock or an external resilience package. In a real application, first identify the operation's semantics and whether repeating it is permitted. A response code alone cannot establish that a prior remote action had no effect.
Trace a transient status followed by success
Attempt one creates a new request and receives ServiceUnavailable. Because another attempt is available, the caller prints retry and continues. Attempt two receives OK, reads ok, prints it and breaks out of the loop. The handler's final counter therefore prints attempts:2. A third request is not sent after success. The maximum of three is a ceiling, not an instruction to perform exactly three calls. Changing only the handler to always return 503 would reach the final attempt and then fail EnsureSuccessStatusCode rather than printing a successful result.
Use a fresh request and dispose each attempt
Each loop body has independent using declarations for its HttpRequestMessage and HttpResponseMessage. The continue and break paths leave that scope and dispose those objects before execution proceeds. The client and externally owned handler span the entire attempt sequence. Creating a fresh request matters because a previously sent request message is not a reusable attempt template. A production request with content also needs a deliberate policy for producing replayable content and releasing each failed response. The small GET example avoids those content-replay complications while keeping ownership visible.
An attempt budget is only one part of resilience
The immediate retry is a classroom simplification for an in-memory handler. It is not a production recommendation to hammer a service without delay. The sample has no backoff, jitter, Retry-After processing, total deadline, concurrency budget or response-size policy. It also does not retry arbitrary exceptions, 404 responses or every non-success status. A production resilience adapter should make these choices explicitly and test them with controlled time. Avoid silently stacking retries at several layers, because multiplying independent attempt budgets can produce substantially more requests than the caller expects.
Practice: prove the terminal failure boundary
Replace the handler with one that always returns 503, and catch the terminal HttpRequestException outside the attempt-owning scopes. Record the number of requests and the terminal status information independently of the exception's localized text. Expect three attempts, no successful result and one final failure. Then change the first response to 404 and verify that the same policy stops after one attempt. The hint is to keep the retry condition narrow instead of letting the test expectation mirror a catch-all branch in the implementation.
Practice: reject an unsafe generalization to writes
Describe a hypothetical POST that creates a reservation, then explain why copying this GET loop unchanged would be insufficient. Design a controlled fake service that records an application idempotency key and can return an uncertain first outcome. Test an explicitly chosen deduplication protocol separately from the transport retry loop. Do not claim a header name alone guarantees deduplication: the server must implement the corresponding contract. The exercise is to specify behavior and evidence, not to call a public purchase or reservation service from the playground.
Review what repeated execution can mean
The useful result is a bounded and observable decision sequence: retry once for the chosen 503 response, stop on the following success and expose the attempt count. Production retries add time and failure policies around that sequence while preserving the meaning of repeated execution. For a GET that retrieves data, verify the service's documented behavior; for an operation that mutates state, establish reconciliation and idempotency semantics before repeating it. Keep failure classification, retry permission, attempt scheduling and final result handling distinguishable in both code and tests.
- Retry only the operations and outcomes allowed by an explicit policy.
- Bound attempts and time separately; dispose every failed response.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using var handler = new RetryHandler(); using var client = new HttpClient(handler, disposeHandler: false);
for (int attempt = 1; attempt <= 3; attempt++) {
using var request = new HttpRequestMessage(HttpMethod.Get, "https://offline.invalid/value");
using var response = await client.SendAsync(request, CancellationToken.None);
if (response.StatusCode == HttpStatusCode.ServiceUnavailable && attempt < 3) { Console.WriteLine("retry"); continue; }
response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync(CancellationToken.None)); break;
}
Console.WriteLine("attempts:" + handler.Attempts);
sealed class RetryHandler : HttpMessageHandler {
public int Attempts { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token) { token.ThrowIfCancellationRequested(); Attempts++; return Task.FromResult(new HttpResponseMessage(Attempts == 1 ? HttpStatusCode.ServiceUnavailable : HttpStatusCode.OK) { Content = new StringContent(Attempts == 1 ? "later" : "ok") }); }
}
Expected output
retry ok attempts:2
An attempt budget and fresh request object do not by themselves make repeating a command safe.