HttpClient
ExceptionHandling
CSharp
.NET
HttpRequestException

Usage of EnsureSuccessStatusCode and handling of HttpRequestException it throws

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

EnsureSuccessStatusCode() is a method on HttpResponseMessage in .NET that throws an HttpRequestException if the HTTP response status code is not in the 2xx range. It provides a quick way to fail fast when an API call returns an error. The alternative is to manually check response.IsSuccessStatusCode and handle failure yourself. Understanding when to use each approach — and how to extract useful information from the exception — is essential for robust HTTP client code.

Basic Usage

csharp
1using System.Net.Http;
2
3var client = new HttpClient();
4
5var response = await client.GetAsync("https://api.example.com/users/1");
6
7// Throws HttpRequestException if status is 4xx or 5xx
8response.EnsureSuccessStatusCode();
9
10var content = await response.Content.ReadAsStringAsync();
11Console.WriteLine(content);

EnsureSuccessStatusCode() checks response.StatusCode. If it is outside the 200-299 range, it throws HttpRequestException and disposes the response content to free resources.

Catching HttpRequestException

csharp
1try
2{
3    var response = await client.GetAsync("https://api.example.com/users/999");
4    response.EnsureSuccessStatusCode();
5    var content = await response.Content.ReadAsStringAsync();
6}
7catch (HttpRequestException ex)
8{
9    Console.WriteLine($"Request failed: {ex.Message}");
10    // Output: "Response status code does not indicate success: 404 (Not Found)."
11
12    // .NET 5+ exposes the status code directly
13    Console.WriteLine($"Status code: {ex.StatusCode}");
14}

In .NET 5 and later, HttpRequestException has a StatusCode property. In earlier versions, you must parse the status code from the exception message or use the alternative approach below.

Alternative: Manual Status Check

csharp
1var response = await client.GetAsync("https://api.example.com/users/999");
2
3if (!response.IsSuccessStatusCode)
4{
5    var errorBody = await response.Content.ReadAsStringAsync();
6    Console.WriteLine($"Error {response.StatusCode}: {errorBody}");
7    // You can read the error response body for details
8    return;
9}
10
11var content = await response.Content.ReadAsStringAsync();
12Console.WriteLine(content);

Manual checking with IsSuccessStatusCode lets you read the response body before handling the error. EnsureSuccessStatusCode() disposes the content, so you cannot read error details after it throws.

Reading Error Body Before Throwing

csharp
1public static async Task EnsureSuccessWithBody(HttpResponseMessage response)
2{
3    if (!response.IsSuccessStatusCode)
4    {
5        var body = await response.Content.ReadAsStringAsync();
6        throw new HttpRequestException(
7            $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}: {body}",
8            null,
9            response.StatusCode  // .NET 5+
10        );
11    }
12}
13
14// Usage
15var response = await client.PostAsync("https://api.example.com/orders", content);
16await EnsureSuccessWithBody(response);

This custom helper reads the error body before throwing, giving you the full API error message in the exception. The built-in EnsureSuccessStatusCode() does not include the response body.

Typed Error Handling

csharp
1public class ApiError
2{
3    public string Code { get; set; }
4    public string Message { get; set; }
5}
6
7public class ApiException : HttpRequestException
8{
9    public ApiError Error { get; }
10    public System.Net.HttpStatusCode Status { get; }
11
12    public ApiException(System.Net.HttpStatusCode status, ApiError error)
13        : base($"API error {status}: {error.Message}")
14    {
15        Status = status;
16        Error = error;
17    }
18}
19
20public static async Task<T> GetAsync<T>(HttpClient client, string url)
21{
22    var response = await client.GetAsync(url);
23
24    if (!response.IsSuccessStatusCode)
25    {
26        var errorJson = await response.Content.ReadAsStringAsync();
27        var error = JsonSerializer.Deserialize<ApiError>(errorJson);
28        throw new ApiException(response.StatusCode, error);
29    }
30
31    var json = await response.Content.ReadAsStringAsync();
32    return JsonSerializer.Deserialize<T>(json);
33}

For APIs that return structured error responses (JSON with error codes and messages), create a custom exception type that deserializes the error body. This gives callers typed access to error details.

Using Polly for Retry with EnsureSuccessStatusCode

csharp
1using Polly;
2using Polly.Extensions.Http;
3
4var retryPolicy = HttpPolicyExtensions
5    .HandleTransientHttpError()  // 5xx and 408
6    .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
7    .WaitAndRetryAsync(3, retryAttempt =>
8        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
9
10var client = new HttpClient();
11
12var response = await retryPolicy.ExecuteAsync(() =>
13    client.GetAsync("https://api.example.com/data"));
14
15response.EnsureSuccessStatusCode();
16var data = await response.Content.ReadAsStringAsync();

Polly integrates with HttpClient to retry transient failures (5xx, timeouts, rate limits) before you call EnsureSuccessStatusCode(). This handles intermittent failures without custom retry logic.

HttpClient with IHttpClientFactory

csharp
1// In Startup.cs / Program.cs
2builder.Services.AddHttpClient("api", client =>
3{
4    client.BaseAddress = new Uri("https://api.example.com/");
5    client.DefaultRequestHeaders.Add("Accept", "application/json");
6});
7
8// In a service class
9public class UserService
10{
11    private readonly HttpClient _client;
12
13    public UserService(IHttpClientFactory factory)
14    {
15        _client = factory.CreateClient("api");
16    }
17
18    public async Task<string> GetUserAsync(int id)
19    {
20        var response = await _client.GetAsync($"users/{id}");
21        response.EnsureSuccessStatusCode();
22        return await response.Content.ReadAsStringAsync();
23    }
24}

IHttpClientFactory manages HttpClient lifetimes and avoids socket exhaustion. Combine it with EnsureSuccessStatusCode() for clean, production-ready HTTP calls.

Common Pitfalls

  • Cannot read response body after EnsureSuccessStatusCode throws: The method disposes the response content before throwing. If you need error details from the API, check IsSuccessStatusCode manually and read the body first.
  • No StatusCode property in .NET Framework / .NET Core < 5: Before .NET 5, HttpRequestException does not expose StatusCode. You must parse it from ex.Message or use the manual check pattern.
  • Catching Exception instead of HttpRequestException: Catching the broad Exception type hides what went wrong. Always catch HttpRequestException specifically so you can distinguish HTTP errors from network failures, timeouts, and other exception types.
  • Not handling timeout separately: HttpClient throws TaskCanceledException (not HttpRequestException) when a request times out. Handle both exception types to cover all failure modes.
  • Using EnsureSuccessStatusCode for expected 404s: If your API returns 404 for "not found" as a normal business response, throwing an exception is heavy-handed. Check response.StatusCode == HttpStatusCode.NotFound explicitly and return null or a default instead.

Summary

  • EnsureSuccessStatusCode() throws HttpRequestException for non-2xx status codes
  • Use it for quick fail-fast behavior when any error is unexpected
  • Use IsSuccessStatusCode manually when you need to read the error response body
  • In .NET 5+, HttpRequestException.StatusCode gives direct access to the HTTP status code
  • Create custom exception types for APIs that return structured error responses
  • Combine with Polly for retry policies on transient HTTP failures

Course illustration
Course illustration

All Rights Reserved.