重试 HttpClient 不成功的请求

Posted

技术标签:

【中文标题】重试 HttpClient 不成功的请求【英文标题】:Retrying HttpClient Unsuccessful Requests 【发布时间】:2013-10-08 23:03:07 【问题描述】:

我正在构建一个给定 HttpContent 对象的函数,它将发出请求并在失败时重试。但是,我收到异常说 HttpContent 对象在发出请求后被释放。无论如何要复制或复制 HttpContent 对象,以便我可以发出多个请求。

 public HttpResponseMessage ExecuteWithRetry(string url, HttpContent content)
 
  HttpResponseMessage result = null;
  bool success = false;
  do
  
      using (var client = new HttpClient())
      
          result = client.PostAsync(url, content).Result;
          success = result.IsSuccessStatusCode;
      
  
  while (!success);

 return result;
 

// Works with no exception if first request is successful
ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, new StringContent("Hello World"));
// Throws if request has to be retried ...
ExecuteWithRetry("http://www.requestb.in/badurl" /*invalid url*/, new StringContent("Hello World"));

(显然我不会无限期地尝试,但上面的代码基本上就是我想要的)。

它会产生这个异常

System.AggregateException: One or more errors occurred. ---> System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.Http.StringContent'.
   at System.Net.Http.HttpContent.CheckDisposed()
   at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context)
   at System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at Submission#8.ExecuteWithRetry(String url, HttpContent content)

是否有复制 HttpContent 对象或重用它的方法?

【问题讨论】:

其他答案很好实现重试。但这里真正的问题是因为您的 HttpContent 在您的帖子之后被处置。您需要在每次重试之前重新创建 StringContent。你不会有这样的 ObjectDisposedException 没错,这里的异常是由于每次发帖后HttpClient都会处理HttpContent造成的。为每个 Polly 执行克隆 HttpContent 是解决方案。可以在here 找到一些可用的克隆扩展。 【参考方案1】:

不要实现包装HttpClient 的重试功能,而是考虑使用在内部执行重试逻辑的HttpMessageHandler 构造HttpClient。例如:

public class RetryHandler : DelegatingHandler

    // Strongly consider limiting the number of retries - "retry forever" is
    // probably not the most user friendly way you could respond to "the
    // network cable got pulled out."
    private const int MaxRetries = 3;

    public RetryHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
     

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        
            response = await base.SendAsync(request, cancellationToken);
            if (response.IsSuccessStatusCode) 
                return response;
            
        

        return response;
    


public class BusinessLogic

    public void FetchSomeThingsSynchronously()
    
        // ...

        // Consider abstracting this construction to a factory or IoC container
        using (var client = new HttpClient(new RetryHandler(new HttpClientHandler())))
        
            myResult = client.PostAsync(yourUri, yourHttpContent).Result;
        

        // ...
    

【讨论】:

请注意,此解决方案不适用于瞬态超时。在这种情况下,似乎在 CancellationToken 上请求取消,这可能会导致抛出 TaskCanceledException。 正如@Gabi 提到的,这不适用于超时。似乎SendAsync 代表一个single 请求操作,因此这不是实现重试机制的正确方法。外部方法效果更好。 我实现了这一点,并在自己的脚下开枪,因为如前所述,它不适用于超时。你不应该像我一样忽视其他评论者,你应该听他们的。 我刚刚尝试过这个,它对我来说很糟糕,因为对base.SendAsync 的调用正在处理传递给client.PostAsyncHttpContent。所以,IME,你的答案不起作用。 (我得到这个答案的唯一原因是避免复制内容才能重试!:-)) 在 .NET Core 中,HttpClient 不再单方面处置HttpContent,因此部分问题消失了。见:github.com/dotnet/corefx/pull/19082/files【参考方案2】:

ASP.NET Core 2.1 答案

ASP.NET Core 2.1 added support 直接为Polly。这里UnreliableEndpointCallerService 是一个在其构造函数中接受HttpClient 的类。失败的请求将以指数回退重试,以便下一次重试在前一次之后的指数更长的时间内发生:

services
    .AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(
        x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)));

另外,请考虑阅读我的博文"Optimally Configuring HttpClientFactory"。

其他平台回答

此实现使用Polly 以指数回退重试,以便下一次重试在前一次之后的指数更长的时间内进行。如果由于超时而抛出HttpRequestExceptionTaskCanceledException,它也会重试。 Polly 比 Topaz 更容易使用。

public class HttpRetryMessageHandler : DelegatingHandler

    public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler) 

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) =>
        Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
            .ExecuteAsync(() => base.SendAsync(request, cancellationToken));


using (var client = new HttpClient(new HttpRetryMessageHandler(new HttpClientHandler())))

    var result = await client.GetAsync("http://example.com");

【讨论】:

自 2016 年 7 月起,Polly 还可以本地处理异常和结果的混合(即自动将某些结果代码视为失败)。因此,上面示例中对 StatusCode 的处理现在可以在 Polly Policy 中本地表达。 Polly readme shows examples @ThiagoSilva 你能不能只在策略中添加一个`.Handle() 来处理取消? retryCount 重命名为retryAttempt 可能会更好。因为前者意味着重试次数(始终为 5),而后者实际上是指当前的重试尝试,在您的示例中从 1 到 5。 要将DelegatingHandler 方法与超时一起使用,请将总体超时(在所有尝试中)与每次尝试的超时区分开来。 HttpClient 上的 Timeout 属性HttpClient.Timeout 将作为所有重试组合的整体超时。要强制每次尝试超时,请使用包装在 WaitAndRetry 策略中的 Polly TimeoutPolicy。此外,Polly 的 TimeoutPolicy 会抛出 TimeoutRejectedException。然后 WaitAndRetry 策略应该处理 TimeoutRejectedException,而不是 TaskCanceledException。这将每次尝试超时与外部取消和所有重试超时区分开来。 @DennisWelu +1,没错。这正是 Polly 的 TimeoutPolicy 引发不同异常 (TimeoutRejectedException) 的原因,因此重试策略可以将其与用户取消区分开来。正如我们在 Polly + HttpClientFactory 文档中所建议的那样,您建议 Dennis(独立取消源)的方法实际上就是将 Polly RetryPolicy 与 TimeoutPolicy 嵌套使用的方法:github.com/App-vNext/Polly/wiki/…。【参考方案3】:

当前的答案在所有情况下都不会按预期工作,特别是在请求超时的非常常见的情况下(请参阅我的 cmets 那里)。

此外,它们实施了一种非常幼稚的重试策略 - 很多时候您想要一些更复杂的东西,例如指数退避(这是 Azure 存储客户端 API 中的默认设置)。

我在阅读related blog post 时偶然发现了TOPAZ(还提供了错误的内部重试方法)。这是我想出的:

// sample usage: var response = await RequestAsync(() => httpClient.GetAsync(url));
Task<HttpResponseMessage> RequestAsync(Func<Task<HttpResponseMessage>> requester)

    var retryPolicy = new RetryPolicy(transientErrorDetectionStrategy, retryStrategy);
    //you can subscribe to the RetryPolicy.Retrying event here to be notified 
    //of retry attempts (e.g. for logging purposes)
    return retryPolicy.ExecuteAsync(async () =>
    
        HttpResponseMessage response;
        try
        
            response = await requester().ConfigureAwait(false);
        
        catch (TaskCanceledException e) //HttpClient throws this on timeout
        
            //we need to convert it to a different exception
            //otherwise ExecuteAsync will think we requested cancellation
            throw new HttpRequestException("Request timed out", e);
        
        //assuming you treat an unsuccessful status code as an error
        //otherwise just return the respone here
        return response.EnsureSuccessStatusCode(); 
    );

注意requester 委托参数。它应该HttpRequestMessage,因为您不能多次发送相同的请求。至于策略,这取决于您的用例。例如,瞬态错误检测策略可以很简单:

private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy

    public bool IsTransient(Exception ex)
    
        return true;
    

关于重试策略,TOPAZ 提供了三种选择:

    FixedInterval Incremental ExponentialBackoff

例如,这是 Azure 客户端存储库默认使用的 TOPAZ:

int retries = 3;
var minBackoff = TimeSpan.FromSeconds(3.0);
var maxBackoff = TimeSpan.FromSeconds(120.0);
var deltaBackoff= TimeSpan.FromSeconds(4.0);
var strategy = new ExponentialBackoff(retries, minBackoff, maxBackoff, deltaBackoff);

更多信息请见http://msdn.microsoft.com/en-us/library/hh680901(v=pandp.50).aspx

编辑请注意,如果您的请求包含 HttpContent 对象,则每次都必须重新生成它,因为 HttpClient 也会处理该对象(感谢您发现 Alexandre Pepin )。例如() =&gt; httpClient.PostAsync(url, new StringContent("foo")))

【讨论】:

或查看Polly。一个更轻量级且恕我直言的更清洁(相对于过度设计)的库来做同样的事情! “你不能多次发送同一个请求”。你能扩展一下吗?我有一个委托处理程序可以做到这一点,它似乎工作得很好。我错过了什么吗? @bornfromanegg 当我尝试传递 HttpRequestMessage 而不是像现在这样的委托时(通常看起来像 () =&gt; client.GetAsync(url))然后第一次尝试会起作用,但以下重试会失败,抛出关于无法重用相同请求消息的异常(我不记得确切的异常类型和措辞) 博客目前已关闭,这里是网络存档的链接web.archive.org/web/20150117043128/http://blog.devscrum.net/…【参考方案4】:

复制 StringContent 可能不是最好的主意。但简单的修改可以解决问题。只需修改函数并在循环内创建 StringContent 对象,如下所示:

public HttpResponseMessage ExecuteWithRetry(string url, string contentString)

   HttpResponseMessage result = null;
   bool success = false;
   using (var client = new HttpClient())
   
      do
      
         result = client.PostAsync(url, new StringContent(contentString)).Result;
         success = result.IsSuccessStatusCode;
      
      while (!success);
      

  return result;
 

然后调用它

ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World");

【讨论】:

这可行,但对于非字符串 httpcontent 类型确实不方便。像多部分形式等。 同意,我也很想禁止内容的处置。但是您需要在某处构造内容对象,那么为什么不在给定函数内部呢? :) httpcontent 对象的创建由我项目中的另一个组件管理,有时它并不重要,并且请求发出逻辑旨在尽可能通用和可重用。如果每次都创建内容,这有点不合时宜。我通过自己复制 httpcontext 来解决这个问题。 这只会重试从服务器返回的带有一些状态代码的请求。很多时候情况并非如此,并且会抛出异常,例如超时。尽管使用外部包装器,但您的想法是正确的。 @VladL 您不应该在循环中处理和重新创建 HttpClient。 HttpClient(尽管它实现了 IDisposable)旨在在应用程序的生命周期中重复使用,否则它将打开一堆套接字。如果你在负载下运行,你最终会得到 SocketExceptions,这意味着你已经用完了套接字。【参考方案5】:

这构建了已接受的答案,但增加了传递重试次数的能力,并增加了向每个请求添加非阻塞延迟/等待时间的能力。它还使用 try catch 来确保在发生异常后继续进行重试。最后,我添加了代码以在 BadRequests 的情况下跳出循环,您不想多次重新发送相同的错误请求。

public class HttpRetryHandler : DelegatingHandler

    private int MaxRetries;
    private int WaitTime;

    public HttpRetryHandler(HttpMessageHandler innerHandler, int maxRetries = 3, int waitSeconds = 0)
        : base(innerHandler)
    
        MaxRetries = maxRetries;
        WaitTime = waitSeconds * 1000; 
    

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        
            try
            
                response = await base.SendAsync(request, cancellationToken);
                if (response.IsSuccessStatusCode)
                
                    return response;
                
                else if(response.StatusCode == HttpStatusCode.BadRequest)
                
                    // Don't reattempt a bad request
                    break; 
                
            
            catch
            
                // Ignore Error As We Will Attempt Again
            
            finally
            
                response.Dispose(); 
            

            if(WaitTime > 0)
            
                await Task.Delay(WaitTime);
            
        

        return response;
    

【讨论】:

这是在 Polly 内部为 TimeoutAsync 政策重新发明***【参考方案6】:

使用 RestEase 和 Task,在多次调用(单例)中重用 httpClient 重试时,它会冻结并抛出 TaskCanceledException。 为了解决这个问题,需要在重试之前 Dispose() 失败的响应

public class RetryHandler : DelegatingHandler

    // Strongly consider limiting the number of retries - "retry forever" is
    // probably not the most user friendly way you could respond to "the
    // network cable got pulled out."
    private const int MaxRetries = 3;

    public RetryHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
     

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        
            response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            if (response.IsSuccessStatusCode) 
                return response;
            

            response.Dispose();
        

        return response;
    

【讨论】:

【参考方案7】:

这是我使用 polly 实现的。

nuget

https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly

https://www.nuget.org/packages/Polly

using Polly;
using Polly.Extensions.Http;

//// inside configure service
services.AddHttpClient("RetryHttpClient", c =>

    c.BaseAddress = new Uri($"configuration["ExternalApis:MyApi"]/");
    c.DefaultRequestHeaders.Add("Accept", "application/json");
    c.Timeout = TimeSpan.FromMinutes(5);
    c.DefaultRequestHeaders.ConnectionClose = true;

).AddPolicyHandler(GetRetryPolicy());

//// add this method to give retry policy
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()

    return HttpPolicyExtensions
        //// 408,5xx
        .HandleTransientHttpError()
        //// 404
        .OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound)
        //// 401
        .OrResult(msg => msg.StatusCode == HttpStatusCode.Unauthorized)
        //// Retry 3 times, with wait 1,2 and 4 seconds.
        .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

【讨论】:

【参考方案8】:

我在使用单元测试和集成测试时进行了尝试和工作。但是,当我实际从 REST URL 调用时,它卡住了。我找到了this interesting post,这解释了为什么它会卡在这条线上。

response = await base.SendAsync(request, cancellationToken);

解决方法是在末尾添加.ConfigureAwait(false)

response = await base.SendAsync(request, token).ConfigureAwait(false);

我还像这样在那里添加了创建链接令牌部分。

var linkedToken = cancellationToken.CreateLinkedSource();
linkedToken.CancelAfter(new TimeSpan(0, 0, 5, 0));
var token = linkedToken.Token;

HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)

    response = await base.SendAsync(request, token).ConfigureAwait(false);
    if (response.IsSuccessStatusCode)
    
        return response;
    


return response;

【讨论】:

【参考方案9】:

您还可以参阅为 .NET HttpClient 构建瞬态重试处理程序。 访问参考KARTHIKEYAN VIJAYAKUMAR帖子。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.SqlClient;
using System.Net.Http;
using System.Threading;
using System.Diagnostics;
using System.Net;
using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling;

namespace HttpClientRetyDemo

    class Program
    
        static void Main(string[] args)
        
            var url = "http://RestfulUrl";
            var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);

            var handler = new RetryDelegatingHandler
            
                UseDefaultCredentials = true,
                PreAuthenticate = true,
                Proxy = null
            ;

            HttpClient client = new HttpClient(handler);
            var result = client.SendAsync(httpRequestMessage).Result.Content
                .ReadAsStringAsync().Result;

            Console.WriteLine(result.ToString());
            Console.ReadKey();

        
    

    /// <summary>
    /// Retry Policy = Error Detection Strategy + Retry Strategy
    /// </summary>
    public static class CustomRetryPolicy
    
        public static RetryPolicy MakeHttpRetryPolicy()
        
            // The transient fault application block provides three retry policies
            //  that you can use. These are:
            return new RetryPolicy(strategy, exponentialBackoff);
        
    

    /// <summary>
    /// This class is responsible for deciding whether the response was an intermittent
    /// transient error or not.
    /// </summary>
    public class HttpTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy
    
        public bool IsTransient(Exception ex)
        
            if (ex != null)
            
                HttpRequestExceptionWithStatus httpException;
                if ((httpException = ex as HttpRequestExceptionWithStatus) != null)
                
                    if (httpException.StatusCode == HttpStatusCode.ServiceUnavailable)
                    
                        return true;
                    
                    else if (httpException.StatusCode == HttpStatusCode.MethodNotAllowed)
                    
                        return true;
                    
                    return false;
                
            
            return false;
        
    

    /// <summary>
    /// The retry handler logic is implementing within a Delegating Handler. This has a
    /// number of advantages.
    /// An instance of the HttpClient can be initialized with a delegating handler making
    /// it super easy to add into the request pipeline.
    /// It also allows you to apply your own custom logic before the HttpClient sends the
    /// request, and after it receives the response.
    /// Therefore it provides a perfect mechanism to wrap requests made by the HttpClient
    /// with our own custom retry logic.
    /// </summary>
    class RetryDelegatingHandler : HttpClientHandler
    
        public RetryPolicy retryPolicy  get; set; 
        public RetryDelegatingHandler()
            : base()
        
            retryPolicy = CustomRetryPolicy.MakeHttpRetryPolicy();
        


        protected async override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        
            HttpResponseMessage responseMessage = null;
            var currentRetryCount = 0;
            //On Retry => increments the retry count
            retryPolicy.Retrying += (sender, args) =>
            
                currentRetryCount = args.CurrentRetryCount;
            ;
            try
            
                await retryPolicy.ExecuteAsync(async () =>
                
                    responseMessage = await base.SendAsync(request, cancellationToken)
                        .ConfigureAwait(false);
                    if ((int)responseMessage.StatusCode > 500)
                    
                        // When it fails after the retries, it would throw the exception
                        throw new HttpRequestExceptionWithStatus(
                            string.Format("Response status code 0 indicates server error",
                                (int)responseMessage.StatusCode))
                        
                            StatusCode = responseMessage.StatusCode,
                            CurrentRetryCount = currentRetryCount
                        ;
                    // returns the response to the main method(from the anonymous method)
                    return responseMessage;
                , cancellationToken).ConfigureAwait(false);
                return responseMessage;// returns from the main method => SendAsync
            
            catch (HttpRequestExceptionWithStatus exception)
            
                if (exception.CurrentRetryCount >= 3)
                
                    //write to log
                
                if (responseMessage != null)
                
                    return responseMessage;
                
                throw;
            
            catch (Exception)
            
                if (responseMessage != null)
                
                    return responseMessage;
                
                throw;
            
        
    

    /// <summary>
    /// Custom HttpRequestException to allow include additional properties on my exception,
    /// which can be used to help determine whether the exception is a transient
    /// error or not.
    /// </summary>
    public class HttpRequestExceptionWithStatus : HttpRequestException
    
        public HttpStatusCode StatusCode  get; set; 
        public int CurrentRetryCount  get; set; 

        public HttpRequestExceptionWithStatus()
            : base()  

        public HttpRequestExceptionWithStatus(string message)
            : base(message)  

        public HttpRequestExceptionWithStatus(string message, Exception inner)
            : base(message, inner)  
    

【讨论】:

链接的网址无效 链接已修复。【参考方案10】:

我几乎有同样的问题。 HttpWebRequest queueing library, which guarantees request delivery 我刚刚更新(参见 EDIT3)避免崩溃的方法,但我仍然需要通用机制来保证消息传递(或在未传递消息的情况下重新传递)。

【讨论】:

【参考方案11】:

我有同样的问题并解决了。这是关于“StringContent”/“HttpContent”

请查看 Amogh Natu 的博客,它可以帮助我解决这个问题

这段代码的问题是,当第一次调用 PostAsync 时 成功但失败了,httpContent 对象就被释放了。这是作为 在 HttpClient 类中设计。请参阅此方法中的注释。 尽管这看起来很奇怪,但他们打算这样做,以便用户 不必明确地这样做,也可以避免相同的请求 多次发布。

所以发生的情况是,当第一次调用失败时,httpContent 是 处置,然后由于我们有重试机制,它试图使帖子 再次调用,现在使用已处理的对象,因此这次调用 失败并出现 ObjectDisposedException。

解决这个问题的一个简单方法是不使用变量来存储 httpContent ,而是在制作时直接创建 http 内容 称呼。像这样的。

http://amoghnatu.net/2017/01/12/cannot-access-a-disposed-object-system-net-http-stringcontent-while-having-retry-logic/

【讨论】:

链接无效。【参考方案12】:

添加一个同时使用 Polly + 重试策略 + 每次重试超时策略的答案,因为最佳答案没有解决这个问题:

Policy
    .Handle<HttpRequestException>()
    .Or<TaskCanceledException>()
    .Or<TimeoutRejectedException>()
    .OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
    .WrapAsync(
        Policy.TimeoutAsync(TimeSpan.FromSeconds(1), delegate (Context ctx, TimeSpan timeSpan, Task task)
        
            // Do some on-timeout action 
            return Task.CompletedTask;
        )
    )
    .ExecuteAsync(() =>
    
        return httpclient.PostAsync(url, httpRequest);
    );

【讨论】:

【参考方案13】:
        //Could retry say 5 times          
        HttpResponseMessage response;
        int numberOfRetry = 0;
        using (var httpClient = new HttpClient())
        
            do
            
                response = await httpClient.PostAsync(uri, content);
                numberOfRetry++;
             while (response.IsSuccessStatusCode == false | numberOfRetry < 5);
        
return response;



        .........

【讨论】:

处理特定异常总是更好,而不是这样做,最终会导致更多问题

以上是关于重试 HttpClient 不成功的请求的主要内容,如果未能解决你的问题,请参考以下文章

使用Apache HttpClient 4.x进行异常重试

httpclient4怎么使用请求超时后的重试?

java: apache HttpClient > 如何禁用重试

httpclient 学习

WebApi系列~HttpClient的性能隐患 - 转

HttpClient 请求时间过长 - 响应状态码不表示成功:404