TIMEOUT HANDLING WITH HTTPCLIENT
https://www.thomaslevesque.com/2018/02/25/better-timeout-handling-with-httpclient/
The problem
If you often use HttpClient to call REST APIs or to transfer files, you may have been annoyed by the way this class handles request timeout. There are two major issues with timeout handling in HttpClient:
- The timeout is defined at the
HttpClientlevel and applies to all requests made with thisHttpClient; it would be more convenient to be able to specify a timeout individually for each request. - The exception thrown when the timeout is elapsed doesn’t let you determine the cause of the error. When a timeout occurs, you’d expect to get a
TimeoutException, right? Well, surprise, it throws aTaskCanceledException! So, there’s no way to tell from the exception if the request was actually canceled, or if a timeout occurred.
Fortunately, thanks to HttpClient‘s flexibility, it’s quite easy to make up for this design flaw.
So we’re going to implement a workaround for these two issues. Let’s recap what we want:
- the ability to specify timeout on a per-request basis
- to receive a
TimeoutExceptionrather than aTaskCanceledExceptionwhen a timeout occurs.
Specifying the timeout on a per-request basis
Let’s see how we can associate a timeout value to a request. The HttpRequestMessage class has a Properties property, which is a dictionary in which we can put whatever we need. We’re going to use this to store the timeout for a request, and to make things easier, we’ll create extension methods to access the value in a strongly-typed fashion:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public static class HttpRequestExtensions{ private static string TimeoutPropertyKey = "RequestTimeout"; public static void SetTimeout( this HttpRequestMessage request, TimeSpan? timeout) { if (request == null) throw new ArgumentNullException(nameof(request)); request.Properties[TimeoutPropertyKey] = timeout; } public static TimeSpan? GetTimeout(this HttpRequestMessage request) { if (request == null) throw new ArgumentNullException(nameof(request)); if (request.Properties.TryGetValue( TimeoutPropertyKey, out var value) && value is TimeSpan timeout) return timeout; return null; }} |
Nothing fancy here, the timeout is an optional value of type TimeSpan. We can now associate a timeout value with a request, but of course, at this point there’s no code that makes use of the value…
HTTP handler
The HttpClient uses a pipeline architecture: each request is sent through a chain of handlers (of type HttpMessageHandler), and the response is passed back through these handlers in reverse order. This article explains this in greater detail if you want to know more. We’re going to insert our own handler into the pipeline, which will be in charge of handling timeouts.
Our handler is going to inherit DelegatingHandler, a type of handler designed to be chained to another handler. To implement a handler, we need to override the SendAsync method. A minimal implementation would look like this:
|
1
2
3
4
5
6
7
8
9
|
class TimeoutHandler : DelegatingHandler{ protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return await base.SendAsync(request, finalCancellationToken); }} |
The call to base.SendAsync just passes the request to the next handler. Which means that at this point, our handler does absolutely nothing useful, but we’re going to augment it gradually.
Taking into account the timeout for a request
First, let’s add a DefaultTimeout property to our handler; it will be used for requests that don’t have their timeout explicitly set:
|
1
|
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(100); |
The default value of 100 seconds is the same as that of HttpClient.Timeout.
To actually implement the timeout, we’re going to get the timeout value for the request (or DefaultTimeout if none is defined), create a CancellationToken that will be canceled after the timeout duration, and pass this CancellationToken to the next handler: this way, the request will be canceled after the timout is elapsed (this is actually what HttpClient does internally, except that it uses the same timeout for all requests).
To create a CancellationToken whose cancellation we can control, we need a CancellationTokenSource, which we’re going to create based on the request’s timeout:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private CancellationTokenSource GetCancellationTokenSource( HttpRequestMessage request, CancellationToken cancellationToken){ var timeout = request.GetTimeout() ?? DefaultTimeout; if (timeout == Timeout.InfiniteTimeSpan) { // No need to create a CTS if there's no timeout return null; } else { var cts = CancellationTokenSource .CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(timeout); return cts; }} |
Two points of interest here:
- If the request’s timeout is infinite, we don’t create a
CancellationTokenSource; it would never be canceled, so we save a useless allocation. - If not, we create a
CancellationTokenSourcethat will be canceled after the timeout is elapsed (CancelAfter). Note that this CTS is linked to theCancellationTokenwe receive as a parameter inSendAsync: this way, it will be canceled either when the timeout expires, or when theCancellationTokenparameter will itself be canceled. You can get more details on linked cancellation tokens in this article.
Finally, let’s change the SendAsync method to use the CancellationTokenSource we created:
|
1
2
3
4
5
6
7
8
9
10
11
|
protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken){ using (var cts = GetCancellationTokenSource(request, cancellationToken)) { return await base.SendAsync( request, cts?.Token ?? cancellationToken); }} |
We get the CTS and pass its token to base.SendAsync. Note that we use cts?.Token, because GetCancellationTokenSource can return null; if that happens, we use the cancellationToken parameter directly.
At this point, we have a handler that lets us specify a different timeout for each request. But we still get a TaskCanceledExceptionwhen a timeout occurs… Well, this is going to be easy to fix!
Throwing the correct exception
All we need to do is catch the TaskCanceledException (or rather its base class, OperationCanceledException), and check if the cancellationToken parameter is canceled: if it is, the cancellation was caused by the caller, so we let it bubble up normally; if not, this means the cancellation was caused by the timeout, so we throw a TimeoutException. Here’s the final SendAsync method:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken){ using (var cts = GetCancellationTokenSource(request, cancellationToken)) { try { return await base.SendAsync( request, cts?.Token ?? cancellationToken); } catch(OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException(); } }} |
Note that we use an exception filter : this way we don’t actually catch the OperationException when we want to let it propagate, and we avoid unnecessarily unwinding the stack.
Our handler is done, now let’s see how to use it.
Using the handler
When creating an HttpClient, it’s possible to specify the first handler of the pipeline. If none is specified, an HttpClientHandleris used; this handler sends requests directly to the network. To use our new TimeoutHandler, we’re going to create it, attach an HttpClientHandler as its next handler, and pass it to the HttpClient:
|
1
2
3
4
5
6
7
8
9
10
|
var handler = new TimeoutHandler{ InnerHandler = new HttpClientHandler()};using (var client = new HttpClient(handler)){ client.Timeout = Timeout.InfiniteTimeSpan; ...} |
Note that we need to disable the HttpClient‘s timeout by setting it to an infinite value, otherwise the default behavior will interfere with our handler.
Now let’s try to send a request with a timeout of 5 seconds to a server that takes to long to respond:
|
1
2
3
|
request.SetTimeout(TimeSpan.FromSeconds(5));var response = await client.SendAsync(request); |
If the server doesn’t respond within 5 seconds, we get a TimeoutException instead of a TaskCanceledException, so things seem to be working as expected.
Let’s now check that cancellation still works correctly. To do this, we pass a CancellationToken that will be cancelled after 2 seconds (i.e. before the timeout expires):
|
1
2
3
4
|
request.SetTimeout(TimeSpan.FromSeconds(5));var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));var response = await client.SendAsync(request, cts.Token); |
This time, we receive a TaskCanceledException, as expected.
By implementing our own HTTP handler, we were able to solve the initial problem and have a smarter timeout handling.
The full code for this article is available here.
TIMEOUT HANDLING WITH HTTPCLIENT的更多相关文章
- Timeout in android httpclient
原文: http://www.cnblogs.com/codingmyworld/archive/2011/08/17/2141706.html /* 从连接池中取连接的超时时间 */ConnMana ...
- HttpClient throws TaskCanceledException on timeout
error msg: HttpClient throws TaskCanceledException on timeout HttpClient is throwing a TaskCanceledE ...
- httpclient超时总结(转)
Httpclient超时 背景: 网站这边多次因为httpclient调用超时时间没设置好导致关掉,影响非常不好,而且问题重复出现,查看网络,没有比较明确介绍httpclient所有超时相关的设置(大 ...
- HttpClient封装方法
//post请求 public static string PostRequest(string url, HttpContent data) { var handler = new HttpClie ...
- java使用HttpClient
HttpClient常用的包有两个 org.apache.http.client以及org.apache.commons.httpclient 我常用的是org.apache.http.client. ...
- httpclient pool帮助类
摘自爬虫类 用于频繁请求减少网络消耗 import java.io.IOException; import java.io.InterruptedIOException; import java.i ...
- 使用HttpClient MultipartEntityBuilder 上传文件,并解决中文文件名乱码问题
遇到一种业务场景,前端上传的文件需要经过java服务转发至文件服务.期间遇到了原生HttpClient怎么使用的问题.怎么把MultipartFile怎么重新组装成Http请求发送出去的问题.文件中文 ...
- 记一次httpclient Connection reset问题定位
问题:某业务系统在运行一段时间后,某个API一定概率偶现Connection reset现象. 问题定位: 首先想到的是要本地复现出这个问题,但一直复现不出来. 1.根据线上问题相关日志判断应该是有部 ...
- 在ASP.NET Core中用HttpClient(一)——获取数据和内容
在本文中,我们将学习如何在ASP.NET Core中集成和使用HttpClient.在学习不同HttpClient功能的同时使用Web API的资源.如何从Web API获取数据,以及如何直接使用Ht ...
随机推荐
- [转]Nginx的负载均衡方式
如果Nginx没有仅仅只能代理一台服务器的话,那它也不可能像今天这么火,Nginx可以配置代理多台服务器,当一台服务器宕机之后,仍能保持系统可用.具体配置过程如下: 1. 在http节点下,添加ups ...
- iOS第三方支付集成
支付宝(alipay)和微信支付(Wechat Pay) 支付宝: 一.总体流程 (1)先与支付宝签约.获得商户ID(partner)和账号ID(seller)(注冊app⽤用) (2)下载对应的公钥 ...
- rabbitMQ rabbitmq-server -detached rabbitmq-server -detached rabbitmq-server -detached
[root@localhost mnesia]# cat /etc/rabbitmq/rabbitmq-env.conf RABBITMQ_MNESIA_BASE=/home/rabbitmq/mne ...
- SpringBoot2 全局异常处理
参考这篇文章里面的几种异常形式: 全局异常处理是个比较重要的功能,一般在项目里都会用到. 大概把一次请求分成三个阶段,来分别进行全局的异常处理. 一:在进入Controller之前,譬如请求一个不存在 ...
- Could not autowire field: private java.lang.Integer com.taotao.sso.service.impl.UserServiceImpl.SSO_
@Value("REDIS_USER_SESSION_KEY") private String REDIS_USER_SESSION_KEY; 1 2 改正: @Value(&qu ...
- JSP之Cookie对象使用
1.写入Cookie //如果用户勾选一个月内自动登录,则将信息保存至Cookie String[] strings=request.getParameterValues("autoLogi ...
- HtmlUnitDriver 网页内容动态抓取
#抓取内容 WebDriver driver = new HtmlUnitDriver(false); driver.get(url); String html = driver.getPageSou ...
- 第三百六十五节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)的基本查询
第三百六十五节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)的基本查询 1.elasticsearch(搜索引擎)的查询 elasticsearch是功能 ...
- Visual Studio主题与配色方案
有个性的开发人员总是喜欢使用属于的主题和配色方案,它们可以看出开发者的个性,更改它们可以缓解审美疲劳,总之选择一个适合自己的解决方案可能极大的增加自己的编码舒适度. 配色方案的选择和使用 手动修改Vi ...
- Java项目性能持续优化中……
尽量使用StringBuilder和StringBuffer进行字符串连接, 参考链接: Java编程中“为了性能”尽量要做到的一些地方