Classify status before assuming content
An HTTP response carries a status code as well as headers and possibly a representation. HttpResponseMessage.IsSuccessStatusCode classifies the 200 through 299 range as success; it does not say that the response contains JSON, that an entity was returned or that an application operation achieved its intended domain outcome. This lesson uses an explicit 204 no-content branch and an explicit 404 branch so those decisions remain visible. The handler is in memory, but the response objects and status classification are real .NET API behavior. There is no live endpoint or network timing in the transcript.
Trace 204 and 404 as different outcomes
The loop first requests /empty. StatusHandler returns HttpStatusCode.NoContent and empty ByteArrayContent. IsSuccessStatusCode prints True, then the NoContent comparison prints no content. The second request targets /missing, whose response has NotFound status and the text not here. The classification prints False and the general branch prints status:404. The program does not print the failure body. Its four-line transcript therefore proves which status branches were selected, not that an error response is empty or that a server did not send useful diagnostic content.
Inspect a response without accidentally discarding it
Every iteration owns its HttpRequestMessage and HttpResponseMessage with using declarations. That matters on both the success path and the rejection path: an unwanted response still holds content resources until its owner disposes it. The client is reused for the whole sequence. The program chooses to inspect status rather than calling EnsureSuccessStatusCode immediately, because a known status can be an ordinary result for the surrounding application. In a different boundary, EnsureSuccessStatusCode can translate non-success status into an exception. Choose that policy consciously and preserve any information the caller actually needs.
Success does not imply JSON or an application result
Do not feed an empty successful response directly into a JSON deserializer and then describe the resulting parse failure as an HTTP failure. A 204 response is a successful no-content outcome; your application needs a corresponding result policy. Conversely, a 200 body can contain syntactically valid JSON that fails your domain rules. A 404 also does not automatically justify retrying: it may be the expected answer for a missing resource. The sample has only two deliberately selected statuses, so its final else branch is a diagnostic display rather than a complete production classifier.
Practice: add a successful representation
Add a third route named /present that returns 200 with a small JSON representation. Extend the caller to distinguish a successful representation from successful no content and from non-success status. Predict the new output before executing the changes. Keep parsing out of the 204 branch. As a hint, classify the status first, then inspect the media type before parsing the 200 body. Do not turn every non-204 response into JSON: include an unsupported representation case if you want to test that boundary as well.
Practice: make failure handling observable
Write a small result adapter that maps /missing into a named NotFound outcome while preserving the original status code. In a separate controlled call, use EnsureSuccessStatusCode on the same status and catch HttpRequestException outside the response-owning scope. Verify the exception's available status information without comparing a localized exception message. This exercise contrasts two legitimate application policies rather than declaring all exceptions wrong. Add an independent assertion that no successful entity is produced for either failure-handling path, and retain disposal when a branch exits early.
Review transport status and domain meaning separately
Status classification is one layer in a larger response contract. A useful caller names the statuses it expects, decides which are ordinary outcomes, and reserves exception translation for the boundary where that decision belongs. It then validates a representation only when a representation is appropriate. The baseline's True/no content and False/status:404 sequence keeps those layers separate. For a real service, test its documented status contract and representation schema together, including empty bodies and unexpected content, without assuming that an offline handler verified the remote implementation.
- Treat 204 as successful no content rather than missing JSON.
- Status, representation and domain validation answer different questions.
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 client = new HttpClient(new StatusHandler());
foreach (string path in new[] { "empty", "missing" }) {
using var request = new HttpRequestMessage(HttpMethod.Get, "https://offline.invalid/" + path);
using var response = await client.SendAsync(request, CancellationToken.None);
Console.WriteLine(response.IsSuccessStatusCode);
if (response.StatusCode == HttpStatusCode.NoContent) Console.WriteLine("no content");
else Console.WriteLine("status:" + (int)response.StatusCode);
}
sealed class StatusHandler : HttpMessageHandler {
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token) { token.ThrowIfCancellationRequested(); return Task.FromResult(request.RequestUri!.AbsolutePath == "/empty" ? new HttpResponseMessage(HttpStatusCode.NoContent) { Content = new ByteArrayContent(Array.Empty<byte>()) } : new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("not here") }); }
}
Expected output
True no content False status:404
Check status and the endpoint’s body contract before interpreting response bytes as a model.