Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using HttpClient.GetFromJsonAsync(), how to handle HttpRequestException based on HttpStatusCode without extra SendAsync calls?

System.Net.Http.Json's HttpClient extension methods such as GetFromJsonAsync() greatly simplifies the routine codes to retrieve json objects from a web API. It's a pleasure to use.

But because of the way it's designed (returning deserialized objects directly), it does not produce any HttpResponseMessage for inspection that allows me to take custom actions based on HttpStatusCode.

Instead, non-success status codes results in a HttpRequestException, which does not appear to offer any properties that expose strongly typed HttpStatusCode. Instead, the status code is included in the exception's Message string itself.

Edit: .NET 5.0 added the HttpRequestException.StatusCode property, so it can now be inspected when calling GetFromJsonAsync.

//old post below

So I've been doing something like this:

try
{
  var cars = await httpClient.GetFromJsonAsync<List<Car>>("/api/cars");
  //...
}
            
catch (HttpRequestException ex)
{
   if (ex.Message.Contains(HttpStatusCode.Unauthorized.ToString()))
   {
     //Show unauthorized error page...
   }
   //...
}

This feels a bit hacky. With the old school way of creating HttpRequestMessage and calling SendAsync, we naturally got the chance to inspect a response's HttpResponseMessage.StatusCode. Adding some of those codes back would defeat the convenient purpose of using the one-liners in System.Net.Http.Json.

Any suggestions here would be greatly appreciated.

like image 389
thankyoussd Avatar asked Dec 20 '20 18:12

thankyoussd


2 Answers

You can use:

// return HttpResponseMessage
var res= await httpClient.GetAsync<List<Car>>("/api/cars")

if (res.IsSuccessStatusCode)
   var cars = res.Content.ReadFromJsonAsync<List<Car>>();
else
   // deal with the HttpResponseMessage directly as you used to

I use a base class like this:

using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;


namespace MyProject.ClientAPI
{
    public abstract class ClientAPI
    {
        protected readonly HttpClient Http;
        private readonly string BaseRoute;

        protected ClientAPI(string baseRoute, HttpClient http)
        {
            BaseRoute = baseRoute;
            Http = http;
        }

        protected async Task<TReturn> GetAsync<TReturn>(string relativeUri)
        {
            HttpResponseMessage res = await Http.GetAsync($"{BaseRoute}/{relativeUri}");
            if (res.IsSuccessStatusCode)
            {
                return await res.Content.ReadFromJsonAsync<TReturn>();
            }
            else
            {
                string msg = await res.Content.ReadAsStringAsync();
                Console.WriteLine(msg);
                throw new Exception(msg);
            }
        }
        
        protected async Task<TReturn> PostAsync<TReturn, TRequest>(string relativeUri, TRequest request)
        {
            HttpResponseMessage res = await Http.PostAsJsonAsync<TRequest>($"{BaseRoute}/{relativeUri}", request);
            if (res.IsSuccessStatusCode)
            {
                return await res.Content.ReadFromJsonAsync<TReturn>();
            }
            else
            {
                string msg = await res.Content.ReadAsStringAsync();
                Console.WriteLine(msg);
                throw new Exception(msg);
            }
        }
    }
}

and then from derived class, we're back to the one-liner

public class MySpecificAPI : ClientAPI
{
    public MySpecificAPI(HttpClient http) : base("api/myspecificapi", http) {}
    
    public async Task<IEnumerable<MyClass>> GetMyClassAsync(int ownerId)
    {
        try
        {
            return GetAsync<IEnumerable<MyClass>>($"apiMethodName?ownerId={ownerId}");
        }
        catch (Exception e)
        {
            // Deal with exception
        }
    }
    
    // repeat for post
}

UPDATE: HANDLING NULL RETURNS

Having encountered a valid scenario where the WebAPI returns null, the line:

return await res.Content.ReadFromJsonAsync<TReturn>();

will throw a Json Deserialization error.

To address this, we need to detect NoContent response (204) and handle accordingly:

if (res.StatusCode == HttpStatusCode.NoContent)
    return default(TReturn);
else if (res.IsSuccessStatusCode)
    return await res.Content.ReadFromJsonAsync<TReturn>();
like image 71
Neil W Avatar answered Oct 07 '22 15:10

Neil W


I just found out that .NET 5.0 actually added the StatusCode Property to the HttpRequestException class!

https://github.com/dotnet/runtime/pull/32455

https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httprequestexception.statuscode?view=net-5.0

like image 10
thankyoussd Avatar answered Oct 07 '22 15:10

thankyoussd