On mobile, bandwidth counts. It is not unlimited and can even impose costs on your users. One way to reduce the bandwidth usage is implementing HTTP conditional requests. In this post, I will explain to you what they are and how to use them.
What Are HTTP Conditional Requests?
First of all: HTTP conditional requests is something that needs to be implemented on both the server as well as the client side. It basically consists of a couple of regular HTTP headers that determine whether data needs to be provided with the response based on the header values. Now, that is a whole mouth full, so let’s just see an example.
Just a quick note before we dive in, the code for this post can be found here: https://github.com/jfversluis/HttpConditionalSample. I use a sample server application which is here: https://github.com/jfversluis/AirplaneModeProof/tree/master/AirplaneModeProof.Api
One of these headers – and the one I will be showing you in this post – is the ETag, short for Entity Tag. The header contains a hash of the data that is sent with the response for your request. For example, if your request returns a JSON object, in the If-None-Match header the hash of that JSON payload is provided as well. The definition of an ETag does not specify how the hash needs to be calculated so you can do it however you want: MD5, SHA-something, it doesn’t matter, as long as it is unique to that data. It is customary to encode it as Base64 in the end.
On the client, you save that hash together with the endpoint you got it from and whenever you sent another request to that endpoint you supply the hash in the If-None-Match header. The server then checks if the hash of the data and the one you client just sent are identical and if so, it will just send an HTTP code 304 response without the data, saving you precious bandwidth.
To demonstrate how all of this worked I have created a little sample app. It might seem familiar if you are following my posts, it has the Avengers theme again. It will retrieve a list of members of the Marvel Avengers team. When the list is pulled to refresh, the data is not sent again, instead, by the means of the ETag, it is determined that the data is unchanged.
If you want to see what is going over the wire, you need a tool like Charles or similar. The screenshots in this post are from HTTPScoop, this tool only works with HTTP calls and isn’t maintained anymore. For simple things, I do still like it though.
On the Server
First, let’s have a quick look at how we implement this functionality server-side. With ASP.NET Core you have this thing called middleware. By implementing our own custom middleware to compute a hash of all the data that is going out, we can easily implement this for all request in our entire application. Luckily, there are some smart people who have already done the hard work for us, so I have shamelessly copied some code created by Mads Kristensen. You can see it underneath.
In short what you see happening here is it checks if the response is going to be an HTTP 200 OK and if it is, it computes an ETag hash. In this case, as SHA-1 and then encoded in Base64. Remember, the way you hash the data does not matter.
To make our server application use this middleware, just wire it up in the Configure method of the Startup.cs file. If we would now make a request to the server and inspect the request/response, you will see the ETag header show up. In the image below you see this behavior.
On the Client
To make this work on the client, we need to save this ETag value locally and send it with our requests from here on out. To make this work for all of our requests like on the server, the easiest thing to do is implement a custom, HttpClientHandler. You can see my implementation below.
What this code does is save the ETag for each endpoint that we are accessing. In this case, the values are stored in memory so they will be discarded when the app is restarted. If you want a more persistent solution you should save it to disk or similar.
Making the Request
The way a custom HttpClientHandler works is like this: everything before the base call is before the request is done. After the base call, is when the response is received. That is why we first check if we have an ETag saved for the endpoint we’re going to query. If we do, enrich the request with the If-None-Match header and the ETag value. Then, after the base call, we see if the request was successful and see if an ETag header is available for us to save.
To use the custom HttpClientHandler we provide it to the default HttpClient and we’re good to go. Now we can just retrieve our data as we would normally do, you can review it below.
In the HttpClient constructor, we can supply our newly created HttpClientHandler and that is all we need to do. The rest of the code should be pretty self-explanatory. One thing that might seem a bit odd is the ignored exception near the end.
When doing a request the second time it will now produce an HTTP 304 (Not Modified), which is technically not a success code and will crash our app. That is why I added a filter for this specific exception.
If we would now do a second request to the same endpoint, the response code is a 304 and the request body will be empty, saving us potentially kilobytes of unnecessary data.
Besides using HTTP conditional requests, there are other measures you can take to limit your requests, like implementing caching. And to make your network requests more robust you can implement a library like Polly, you can read about it here.
The ETag header is not the only condition that can be used. Others are If-Match, If-Modified-Since, If-Unmodified-Since, If-Range. You can read more on it in the Mozilla documentation here.
Implementing HTTP conditional requests isn’t a very hard thing to do and can save your users a lot of bandwidth over time. If that is something that is important to you, then this might be something that is interesting to look at. The sample code can be found here: https://github.com/jfversluis/HttpConditionalSample. This already contains an address to a hosted server application that you are free to use.