We're working on a large project which consumes a custom-build API, and one of the requirements of this project is that if the API goes down, certain sections of the website still need to be able to function.
The reason for this is that our app consumes a lot of APIs and services, and occasionally a non-critical one will go down (for maintenance, bugs, whatever) and we don't want our site to crash as well, since this was a problem that plagued the previous version of this app.
To do this, we needed a way to ensure that if a non-critical service or API went down, we could record the fact that a request to said service timed out and NOT throw up a scary-looking error page to our users. We're using the incredible RestSharp library to consume our API, so it occurred to me that the best place for this timeout-handler to exist was in some kind of common class that extended RestSharp's basic functionality.
This post lays out what we built in order to record timeouts from our APIs and services (and, as always, a demo application can be found over on GitHub).
API Setup
I've set up the sample project in much the same way that our real project is set up. Closest to the data store (e.g. the "bottom" layer of the stack) is the API, which will create data for our ASP.NET MVC application to consume.
For this demo, let's create a simple API which returns the current date and time.
[RoutePrefix("home")]
public class HomeController : ApiController
{
[HttpGet]
[Route("date")]
public IHttpActionResult GetDate()
{
var rand = new Random();
Thread.Sleep(rand.Next(1, 100));
return Ok(DateTime.Now);
}
}
The call to Thread.Sleep()
exists to show that we can handle situations where the API doesn't respond. Any call to this API method will be delayed between 1 and 100 milliseconds before responding. (Just to be clear, don't use that call in production applications.)
Building the Base Client
In order for our ASP.NET MVC app to consume our API, we build classes known as clients, which implement an interface from RestSharp called IRestClient
. Normal usage of RestSharp dictates that we create a class which implements that IRestClient
interface.
But we have a different scenario than is covered by the normal usage. We need to determine a way to ensure that our clients can handle timeouts, and in a uniform manner. To accomplish this, let's implement a base client class from which all our other clients will derive.
public class ClientBase : RestSharp.RestClient
{
public ClientBase(string baseUrl)
{
BaseUrl = new Uri(baseUrl);
}
}
Notice that the base class inherits from RestClient
rather than implementing IRestClient
.
This base client will need to override the existing Execute()
methods in RestClient
to have them call our Timeout checking method. Here's the four methods we want to implement (as well as the implementation of the Timeout check):
public class ClientBase : RestSharp.RestClient
{
public ClientBase(string baseUrl)
{
BaseUrl = new Uri(baseUrl);
}
public override IRestResponse<T> Execute<T>(IRestRequest request)
{
var response = base.Execute<T>(request);
TimeoutCheck(request, response);
return response;
}
public override async Task<IRestResponse> ExecuteTaskAsync(IRestRequest request)
{
var response = await base.ExecuteTaskAsync(request);
TimeoutCheck(request, response);
return response;
}
public override async Task<IRestResponse<T>> ExecuteTaskAsync<T>(IRestRequest request)
{
var response = await base.ExecuteTaskAsync<T>(request);
TimeoutCheck(request, response);
return response;
}
private void TimeoutCheck(IRestRequest request, IRestResponse response)
{
if (response.StatusCode == 0)
{
//Uncomment the line below to throw a real exception.
//throw new TimeoutException("The request timed out!");
}
}
}
We overrode these particular methods to handle four scenarios:
Execute()
handles situations in which we don't expect response data (meaning we will get aIRestResponse
object with things like the HTTP status code, but we won't get any further data out of it).Execute<T>()
handles situations in which we DO expect response data from the API.ExecuteTaskAsync()
handles scenarios in which we want to asynchronously call the API and don't expect any response data.ExecuteTaskAsync<T>()
handles the final scenario, in which we want to asynchronously call the API and expect to get response data back.
I want to draw particular attention to the TimeoutCheck()
method. As far as I can tell, the way RestSharp denotes a timeout is to give the generated Response object a StatusCode of 0. This seems like it could also represent other scenarios, and so if anyone out there has a better way for me to specifically identify timeouts, I'd love to hear it.
Anyway, with the base class now implemented, we can build out client.
Building the Clients
We need to build a specific Client which will consume the API method (GetDate()
) that we defined earlier. This class will need to both inherit from ClientBase
(to get the custom method we wrote in the previous sections) and implement IRestClient
(to get the additional functionality provided by RestSharp).
For completeness's sake, we will also implement asynchronous and synchronous versions of the same call.
Here's the complete DateClient
class:
public class DateClient : ClientBase, IRestClient
{
public DateClient() : base(Constants.BaseApiUrl) { }
public DateTime? GetDate()
{
var request = new RestRequest("/home/date");
request.Timeout = 50;
return Execute<DateTime?>(request).Data;
}
public async Task<DateTime?> GetDateAsync()
{
var request = new RestRequest("/home/date");
request.Timeout = 50;
var result = await ExecuteTaskAsync<DateTime?>(request);
return result.Data;
}
}
Notice that the request.Timeout
property is set to 50 milliseconds. Remember that our API will delay for a random amount of milliseconds between 1 and 100. This setup ensures that we can test our ability to handle timeouts. In the real world, you would set the Timeout property to something considerably more sane than 0.05 seconds.
More importantly, note that these methods return a nullable DateTime?
, whereas the API method returns the non-nullable DateTime
. This is because if RestSharp does not get a response, the IRestResponse.Data
property will be set to the default value of the object it is trying to deserialize (e.g. the T
in Execute<T>()
). Because we set it to nullable, we automatically know that a null value is the error case, and can handle it accordingly.
Completing the Demo
We only need a couple more pieces to complete the demo. First, let's build an MVC controller which implements both the synchronous and asynchronous versions of our Client methods:
[RoutePrefix("home")]
public class HomeController : Controller
{
[HttpGet]
[Route("index")]
public ActionResult Index()
{
var client = new DateClient();
var model = new HomeIndexVM();
model.CurrentDate = client.GetDate();
return View(model);
}
[HttpGet]
[Route("async")]
public async Task<ActionResult> Async()
{
var client = new DateClient();
var model = new HomeIndexVM();
model.CurrentDate = await client.GetDateAsync();
return View("Index", model);
}
}
We also need to build a simple Index.cshtml view to display the current date and time:
@model RestSharpTimeoutDemo.MVC.ViewModels.Home.HomeIndexVM
@{
ViewBag.Title = "Home Page";
}
@if (Model.CurrentDate.HasValue)
{
<h1>The current date/time is @Model.CurrentDate</h1>
}
else
{
<h1>The request timed out!</h1>
}
Finally, we can run the app. Sometimes we will get the error version of this page:
But, when the call doesn't timeout, we will correctly display the date and time.
The important thing is that either way, the user is not taken to some unhelpful error page (or, God forbid, the Yellow Screen of Death), and instead remains in the app where s/he can actually get some work done and not be blocked because we couldn't display the current date and time. If our goal was to get out of the user's way, I daresay this achieves that feat.
Summary
RestSharp is wonderfully extensible, and we took advantage of that to create a common client that handles timeouts. This client allows us to step out of our user's way to provide a more seamless user experience by, essentially, not throwing up a big yellow flag which screams SOMETHING WENT WRONG. And that, I feel, is a win.
If you want to dive deeper into this problem (or see something you could improve), check out the sample project on GitHub!
Happy Coding!