It is quite a possible situation to have a user navigating to the client application’s page that sends an HTTP request to the server. While our app processes the request, a user can navigate away from that page. In such a case, we want to cancel the HTTP request since the response is no longer important to that user. Of course, this is just one of many situations that could happen in a real-world application where we would want to cancel our request. So, in this article, we are going to learn how to use CancellationToken to cancel HTTP requests in our client application.
You can also visit our HttpClient Tutorial page, to see all the articles from this tutorial.
So, let’s dive right into it.
Using CancellationToken to Cancel Requests Sent with HttpClient
In the introduction, we stated that if a user navigates away from a page, they need the response no more, and thus it is a good practice to cancel that request. But there is more than that. HttpClient is working with async Tasks, therefore canceling a task that is no longer needed will set free the thread that we use to run the task. This means that the thread is going to be returned to a thread pool where this thread can be used for some other work. This will improve the scalability of our application for sure.
Of course, we can’t cancel the request just like that. To execute such an action we have to use CancellationTokenSource and CancellationToken.
We use CancellationTokenSource to create CancellationToken and to notify all the consumers of the CancellationToken that the request has been canceled. In our case, the HttpClient will consume the CancellationToken and listen for the notifications. As soon as the request cancelation notification is received, we are going to cancel that request using the HttpClient.
So, let’s see how to do that.
Implementing CancellationToken Logic with HttpClient
The first thing we are going to do is to create a new service for this example:
public class HttpClientCancellationService : IHttpClientServiceImplementation { private static readonly HttpClient _httpClient = new HttpClient(); private readonly JsonSerializerOptions _options; public HttpClientCancellationService() { _httpClient.BaseAddress = new Uri("https://localhost:5001/api/"); _httpClient.Timeout = new TimeSpan(0, 0, 30); _httpClient.DefaultRequestHeaders.Clear(); _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; } public async Task Execute() { throw new NotImplementedException(); } }
So, this is a familiar code that we have in our previous services as well. We create an HttpClient instance and provide a configuration for it. Also, we do the same with the JSON serialization options. In the next article, we are going to learn about the HttpClientFactory
and see how we can move this configuration to a single place without repeating it in all the files, and also learn how to solve problems that HttpClient can cause. For now, we are going to leave it as-is.
Now, let’s add a new method to fetch all the companies:
private async Task GetCompaniesAndCancel() { using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
This is also a familiar code from the previous article and we won’t be explaining it here. If you are not familiar with streams, you can always read the linked article.
Now, let’s assume we want to cancel this request. As we already said, to cancel a request we need the CancellationTokenSource. So, let’s implement it:
private async Task GetCompaniesAndCancel() { var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.CancelAfter(2000); using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
Here, we create a new cancellationTokenSource
object. For this to work, we have to include the using System.Threading.Tasks
namespace. After we create the object, we want to cancel the request. This is usually executed by the user – by pressing the cancel button or navigating away from a page, but for the example purpose, we are going to do it here. To cancel a request, we can use two methods: Cancel()
, which cancels the request immediately, and CancelAfter()
. For this example, we use the CancelAfter
method and provide two seconds as an argument. Finally, we have to notify the HttpClient about the cancellation action. To do that, we provide a cancellation token as an additional argument for the GetAsync shortcut
method.
That’s it. We can test it now.
Testing Cancelling the Request
Before we start our applications, we need to make sure that our method gets called when the app starts. To do that, we have to modify the Execute
method:
public async Task Execute() { await GetCompaniesAndCancel(); }
Also, we have to register this service in the Program
class:
private static void ConfigureServices(IServiceCollection services) { //services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientPatchService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientStreamService>(); services.AddScoped<IHttpClientServiceImplementation, HttpClientCancellationService>(); }
Now, let’s start both applications:
And, we can see that our request was canceled. If you want to test it again, make sure to restart the API as well, so it can simulate the longer first request.
Improving the Solution by Sharing the CancellationToken
The implementation as-is works great for our learning example. But in a real-world application, we would like to be able to cancel different requests by passing the token to all of them. This would enable canceling all of these requests if we need to. Also, we would like to be able to access this CancellationTokenSource from different parts of the application, for example when the user clicks the cancel button or navigates away from the page. In that case, we don’t want to hide the CancellationTokenSource inside a single method.
That said, let’s add some modifications to our service:
private static readonly HttpClient _httpClient = new HttpClient(); private readonly JsonSerializerOptions _options; private readonly CancellationTokenSource _cancellationTokenSource; public HttpClientCancellationService() { _httpClient.BaseAddress = new Uri("https://localhost:5001/api/"); _httpClient.Timeout = new TimeSpan(0, 0, 30); _httpClient.DefaultRequestHeaders.Clear(); _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; _cancellationTokenSource = new CancellationTokenSource(); }
Here, we create a CancellationTokenSource readonly variable and instantiate it in the constructor.
Then, we want to modify the Execute
method:
public async Task Execute() { _cancellationTokenSource.CancelAfter(2000); await GetCompaniesAndCancel(_cancellationTokenSource.Token); }
In this method, we call the CancelAfter
method to specify the period after which we want to cancel our request and also pass the token to the GetCompaniesAndCancel
method.
Of course, we have to modify the GetCompaniesAndCancel
method as well:
private async Task GetCompaniesAndCancel(CancellationToken token) { using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead, token)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
As you can see, at this point, our method accepts the token and use it to listen for the cancel notifications.
Now, we can start again our API and also the client app.
As soon as we do that, we are going to see the same exception:
That’s good.
We can continue to see how to handle this exception in our application.
Handling TaskCanceledException
If we want to handle the exception that our application throws after canceling a request, all we have to do is to wrap our request inside the try-catch block:
private async Task GetCompaniesAndCancel(CancellationToken token) { try { using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead, token)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } } catch (OperationCanceledException ocex) { Console.WriteLine(ocex.Message); } }
We saw that the app throws the TaskCanceledException
but since it inherits from the OperationCanceledException
class, we can use that class to catch our exception. Of course in the catch block, we can do a lot of actions but for this example, it is enough to just log the message.
Now, let’s start both applications and inspect the result:
Great.
Inspecting Status Codes from Responses
With the implementation as we have it right now if the response is not successful, we are going to throw an exception. Well, to be 100% accurate the EnsureSuccessStatusCode()
method will do it. But in many cases, we want to ensure a more user-friendly message depending on the real reason behind the response failure. Well, for that, we can check the status codes of our response.
We are not going to cover all the status codes here, for that you can visit our HTTP Reference table. That said, here we are going to use one of the status codes and show how to provide a better user experience with more meaningful messages.
For this example, we are going to use the HttpClientStreamService
class. So, let’s create a new method in that class:
private async Task GetNonExistentCompany() { var uri = Path.Combine("companies", "F8088E81-7EFA-4E49-F824-08D8C38D155C"); using (var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
This entire code is now familiar to us, but we just want to mention that the provided Id (a Guid value) doesn’t exist in our database. So, our API should return not found result (404).
Before we test it, we have to modify the Execute
method:
public async Task Execute() { //await GetCompaniesWithStream(); //await CreateCompanyWithStream(); await GetNonExistentCompany(); }
And also, we have to enable this service in the Program
class:
private static void ConfigureServices(IServiceCollection services) { //services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientPatchService>(); services.AddScoped<IHttpClientServiceImplementation, HttpClientStreamService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientCancellationService>(); }
Great.
Let’s start both applications and inspect the result:
As you can see, we did get the 404 response but we still throw an exception. Well, we can change that.
Working with Status Codes
Let’s add a small modification to our method:
private async Task GetNonExistentCompany() { var uri = Path.Combine("companies", "F8088E81-7EFA-4E49-F824-08D8C38D155C"); using (var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)) { if(!response.IsSuccessStatusCode) { if (response.StatusCode.Equals(HttpStatusCode.NotFound)) { Console.WriteLine("The company you are searching for couldn't be found."); return; } response.EnsureSuccessStatusCode(); } var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
We first check if the response doesn’t contain a successful status code with the IsSuccessStatusCode
property. If it doesn’t, we explicitly check for the status code we want to handle, in this case, a NotFound
status code. In that case, we just write an informative message to a console window. For all the other unsuccessful status codes, we throw an exception with the EnsureSuccessStatusCode
method.
Of course, you can always expand this conditioning with other status codes as well, but in that case, it would be better to extract that logic into another method to make this method more readable.
Now, if we start our applications:
We can see our message on the screen.
Everything works as expected.
Conclusion
There we go.
Now, we know how to cancel our request using the CancellationToken and CancellationTokenSource and also how to use CancellationTokenSource to share the token between different requests. Furthermore, we know how to use different status codes from our response to prevent throwing exceptions for each unsuccessful response.
In the next article, we are going to learn more about HttpClientFactory, and see what are the advantages of that approach.
Until then.
Best regards.