背景

用了那么多年的HttpWebRequest,没想到今天就被这么狠狠的坑了一波。
直接放代码,来看看今天的罪魁祸首HttpWebRequest.Abort()方法。

本文基于.NET4.5框架

在实际项目中,Http请求部分用的异步接口IAsyncResult实现的,但是为了缩短篇幅,这里改为Task来举例。

 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
28
29
30
31
32
HttpWebRequest request = null;
Task.Run(() =>
{
    try
    {
        request = (HttpWebRequest)WebRequest.Create("http://www.jiuling.com");
        using (WebResponse response = request.GetResponse())
        {
            //模拟一个耗时请求
            Thread.Sleep(10 * 000);
            using (Stream stream = response.GetResponseStream())
            {
                using (StreamReader streamReader = new StreamReader(stream, Encoding.UTF8))
                {
                    string html = streamReader.ReadToEnd();
                    //Do succeed callback
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("请求失败咯,准备回调");
        //Do failing callback
    }
});

//模拟一个等待超时的情况
Thread.Sleep(5 * 1000);
request.Abort();
Console.WriteLine("请求超时咯,准备回调");
//Do failing callback

上面的代码主要做了这么几件事:

  • 请求成功后,调用一个成功的回调
  • 请求出现异常时,调用一个失败的回调
  • 请求出现超时时,调用一个失败的回调

今天出问题的订单就是收到了两次失败回调

问题分析

由于两次的失败回调间隔很短(大概20ms的样子),所以我当时直接排除了代码进行了两次Http请求。结合我们的业务代码,当时猜想应该是有个地方捕获了异常并进行了处理(失败回调),接着又将异常抛出,导致外层代码也处理了一次失败回调
经过层层排查,后面确定了我的猜想失败了。导致问题的原因是由于request.Abort()引发了一次WebException异常,从而使得代码进行了两次回调(上面代码中注释的部分)

虽然我以前经常用HttpWebRequest,但是对Abort方法的认知却是少之又少,甚至可以说是没怎么用过。今天的问题刚好是这个方法引起的,又是自己的知识盲区,那么刚好研究下这个坑货。😎😎

先来看看MSDN上的说明:

哦,上帝啊,这该死的机翻,这可是MS的官网啊,他们怎么能这样呢🐶🐶
Abort.png

翻译成人话就是:Abort方法用于取消对资源的请求,调用Abort方法后,如果再调用GetResponseBeginGetResponseEndGetResponseGetRequestStreamBeginGetRequestStreamEndGetRequestStream方法,将会引发一个WebException异常,并且该异常的Status值为RequestCanceled

原来MSDN写的清清楚楚明明白白。
我们并不是被Abort方法坑了,写上面那段代码的人对这个方法不熟悉,埋了一个坑;今天排查问题时,我对这个方法也不熟悉,导致没有第一时间发现问题。该反思、该反思!

问题复现

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
HttpWebRequest request = null;
Task.Run(() =>
{
    try
    {
        Console.WriteLine("准备发起请求");
        request = (HttpWebRequest)WebRequest.Create("http://www.jiuling.com");
        using (WebResponse response = request.GetResponse())
        {
            Console.WriteLine("GetResponse");
            using (Stream stream = response.GetResponseStream())
            {
                Console.WriteLine("GetResponseStream");
                using (StreamReader streamReader = new StreamReader(stream, Encoding.UTF8))
                {
                    //假装这次网络请求超级慢~~
                    Thread.Sleep(5000);

                    Console.WriteLine("ReadData");
                    string html = streamReader.ReadToEnd();

                    //如果代码执行到这里的时候,调用Abort方法则不会引发异常
                    //Thread.Sleep(5000);
                    Console.WriteLine($"服务器返回:{html}");
                }
            }
        }
    }
    catch (WebException ex)
    {
        Console.WriteLine($"出错了,状态:{ex.Status},错误信息:{ex.Message}");
    }
});
Thread.Sleep(2 * 1000);
Console.WriteLine("准备Abort");
request.Abort();
Console.WriteLine("Abort完成");

// 代码输出:

// 准备发起请求
// GetResponse
// GetResponseStream
// 准备Abort
// Abort完成
// ReadData
// 出错了,状态:RequestCanceled,错误信息:请求被中止: 请求已被取消。

这个例子中,调用Abort的时候虽然服务器已经响应,但是实际上执行streamReader.ReadToEnd()方法的时候才会去读取数据流,因此依然会引发一个WebException异常。

总结

总结一下就是:

  • 当网络请求未完成时,如果调用Abort方法,则会引发一个WebException异常
  • 遇到不懂的技术点、不懂的代码,不要用在实际项目中(类似已经出过好多事了!!)
  • 好好学习天天向上
  • 编不下去了