8

I'm calling my Web API using an HttpClient and I see that there is an EnsureSuccessStatusCode method and an IsSuccessStatusCode property. Which one is appropriate?

I read this article and I have a few additional questions:

Usage of EnsureSuccessStatusCode and handling of HttpRequestException it throws

The issue I'm having is that if I send a GET request where I pass the ID of the object I want to retrieve, there are basically two outcomes:

  1. I get the object back with a status 200
  2. I could get null back because there was no match, however this results in a status of 404.

If I call EnsureSuccessStatusCode() a status 404 would result in an exception being thrown. That isn't ideal because when I was testing my code I kept getting a 404 error which at first made me think that the API URL was incorrect, but in reality there was no matching object with the supplied Id. In this case, I'd rather return a null object instead of throwing an exception.

So instead I tried checking the value of the IsSuccessfulStatusCode property. That seemed like a better choice and I can return a null object when this property is false, however, there are many status codes that can result in this property having a false value. 404 is one of them, but there are several other status codes, such as 400 Bad Request, 405 Method Not Allowed, etc. I'd like to log an exception for all unsuccessful error codes except for the 404's and I'm wondering if there is a better way to do this than inspecting the ResponseCode value of the response and then throwing an exception which gets caught by my catch block, which is where the logging takes place.

Here's the code for my GET method:

public static Customer GetCustomerByID(int id)
{
    try
        {
            using (var client = GetConfiguredClient())
            {
                Customer customer = null;
                var requestUri = $"Customers/{id}";

                using (var response = client.GetAsync(requestUri).Result)
                {
                    if (response.IsSuccessStatusCode)
                        customer = response.Content.ReadAsAsync<Customer>().Result;
                }

                return customer;
            }
        }
        catch (Exception ex)
        {
          ex.Data.Add(nameof(id), id);

          LogException(ex);

          throw;
        }
    }

This code will just return a null Customer if a non-successful status code is returned and nothing gets logged.

What is the best way to handle this situation?

CodeNotFound
  • 22,153
  • 10
  • 68
  • 69
DesertFoxAZ
  • 439
  • 1
  • 4
  • 14
  • using `EnsureSuccessStatusCode()` is bad idea because: **All the information about why your HTTP call failed is lost!!** you don't know it fails due to Authentication issues, Network issues, Server side issues, DNS related issues...? – S.Serpooshan Jun 05 '23 at 18:18

3 Answers3

9

The accepted answer employs "branching by exception", which is considered an anti-pattern by some. Here is how to use EnsureSuccessStatusCode and IsSuccessStatusCode in a way that uses exceptions only for errors that are unexpected or cannot or should not be handled "locally":

  1. If you want to handle specific error responses, handle them directly using if-statements.
  2. If you want to treat all (remaining) error responses as unexpected errors, use EnsureSuccessStatusCode and do not catch the exception, but assume it will be handled by a catch handler that can actually do something about it (such as higher level application logic, or a generic top level error handler).
  3. If you want to do something for all (remaining) error responses (such as logging), but then proceed normally, or if you want to throw your own type of exception, use IsSuccessStatusCode.

This approach gives you all the advantages of exceptions while minimizing the disadvantages (such as breaking in the debugger on a completely normal event that you might not be interested in, or riddling your code with catch blocks, which are harder to read and write than if-statements).

Example:

using (var response = client.GetAsync(requestUri).Result)
{
  if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
  {
    // TODO: Special handling for "401 Unauthorized" goes here
  }
  else
  {
    // All other unsuccessful error codes throw
    response.EnsureSuccessStatusCode();

    // TODO: Handling of successful response goes here
  }
}

... or if you want to read the error response or do logging, etc.:

using (var response = client.GetAsync(requestUri).Result)
{
  if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
  {
    // TODO: Special handling for "401 Unauthorized" goes here
  }
  else if (!response.IsSuccessStatusCode)
  {
    // TODO: Read error response, logging, throw custom exception, etc., goes here

    // TODO: Keep this if you still want to throw the standard exception.
    // TODO: Otherwise, remove this.
    response.EnsureSuccessStatusCode();
  }
  else
  {
    // TODO: Handling of successful response goes here
  }
}
Florian Winter
  • 4,750
  • 1
  • 44
  • 69
5

Because of this:

So instead I tried checking the value of the IsSuccessfulStatusCode property. That seemed like a better choice and I can return a null object when this property is false, however, there are many status codes that can result in this property having a false value. 404 is one of them, but there are several other status codes, such as 400 Bad Request, 405 Method Not Allowed, etc. I'd like to log an exception for all unsuccessful error codes except for the 404's and I'm wondering if there is a better way to do this than inspecting the ResponseCode value of the response and then throwing an exception which gets caught by my catch block, which is where the logging takes place.

I would use the EnsureSuccessStatusCode method and then modify the catch block like below:

public static Customer GetCustomerByID(int id)
{
    try
    {
        using (var client = GetConfiguredClient())
        {
            var requestUri = $"Customers/{id}";
            Customer customer;

            using (var response = client.GetAsync(requestUri).Result)
            {
                try 
                {
                    response.EnsureSuccessStatusCode();
                    // If we reach here it means we can get the customer data.
                    customer = response.Content.ReadAsAsync<Customer>().Result;
                }
                catch(HttpRequestException)
                {
                    if(response.StatusCode == HttpStatusCode.NotFound) // 404
                    {
                        customer = null;
                    }
                    else
                    {
                        throw;
                    }
                }
            }

            return customer;
        }
    }
    catch (Exception ex)
    {
        ex.Data.Add(nameof(id), id);

        LogException(ex);

        throw;
    }
}
Hooligancat
  • 3,588
  • 1
  • 37
  • 55
CodeNotFound
  • 22,153
  • 10
  • 68
  • 69
  • 4
    This is pattern is known as branching by exception and is considered bad practice by some. – Zach Johnson Nov 28 '18 at 15:57
  • 1
    @ZachJohnson Can't be that bad of a practice. Microsoft built this exact thing into c#7 as a feature via the 'when' clause you can add to a Catch. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/when – Bill Tarbell Dec 31 '18 at 09:30
  • 2
    I think inferring that the addition of the when clause does not imply that your code now branches by exception. You are branching by exception when you are structuring your code in a way that you are throwing exceptions and then swallowing them in your catch statement. Effectively using the exception state like a positive if statement. That's not what try/catch statements 'should' be used for. The when clause just adds granularity to what exceptions you want to catch. – Zach Johnson Jan 09 '19 at 21:48
  • 4
    I don't like `EnsureSuccessStatusCode` because it disposes of the `response.Content` object. An error response may be rich with details about the error. You lose all that if you use `EnsureSuccessStatusCode`. All you're left with is the `StatusCode` in the exception blocks, which can only tell part of the story. That's my 2c. – onefootswill Dec 13 '19 at 01:21
  • 2
    +1 @onefootswill - I ran into a problem today where the developer called `response.EnsureSuccessStatusCode();` prior to reading `response.Content`. In my case, this hid an issue where Autofac was missing a registration for a type. I think the bigger question is whether its ever unsafe or a bad idea to try to read the `response.Content` when a request does not have a successful status code. – John Zabroski Aug 18 '20 at 20:06
  • 2
    If you are going to always catch an exception, consider not throwing it. Branching by exception can be annoying when debugging if you enabled "break on exception" to find the cause of an exception that is actually an error. – Florian Winter Nov 25 '20 at 10:36
1

According to this documentation: https://learn.microsoft.com/en-us/windows/uwp/networking/httpclient can be ok following sollution:

Uri requestUri = new Uri("http://www.contoso.com");

//Send the GET request asynchronously and retrieve the response as a string.
Windows.Web.Http.HttpResponseMessage httpResponse = new 
Windows.Web.Http.HttpResponseMessage();
string httpResponseBody = "";

try
{
    //Send the GET request
    httpResponse = await httpClient.GetAsync(requestUri);
    httpResponse.EnsureSuccessStatusCode();
    httpResponseBody = await httpResponse.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
    httpResponseBody = "Error: " + ex.HResult.ToString("X") + " Message: " + ex.Message;
}

BUT: httpResponse.EnsureSuccessStatusCode(); == status codes from 200 to 299

I'm using HttpStatusCode.OK = 200 = is SUB-PART of EnsureSuccessStatusCode and indicates that the request succeeded and the requested information is in the response.

HttpResponseMessage result = await this._httpClient.GetAsync("https://www.web1.com/getThis", cancellationToken);
if (result.StatusCode != HttpStatusCode.OK)
{
    return new HttpResponseMessage(result.StatusCode)
    {
        Content = new StringContent( "ERROR DESCRIPTION HERE")
    };
}
return result;  // I can return here HttpResponseMessage....
Miroslav Siska
  • 401
  • 4
  • 15
  • I am not sure about if httpResponse.EnsureSUccessStatusCode() equal to status codes from 200 to 299, actually I find it will throw an exception if status code is 204 – Loic Sep 24 '20 at 06:31
  • Status code info 200-299 can you found here: https://learn.microsoft.com/en-us/uwp/api/windows.web.http.httpresponsemessage.ensuresuccessstatuscode?view=winrt-19041 – Miroslav Siska Nov 30 '20 at 20:03
  • it says 204 means “The request has been successfully processed and that the response is intentionally blank.”, I think it is a success request. But in EnsureSuccessStatusCode it will throw an exception, this is what got me confused. – Loic Dec 02 '20 at 06:08