You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET并行调用API遇连接意外关闭问题求助

问题:ASP.NET MVC调用Core Web API批量生成PDF时请求过多导致连接异常

场景说明

主应用为ASP.NET MVC(.NET 4.8),调用ASP.NET Core 8 Web API生成PDF文档。批量生成时先同步完成所有国家的数据准备与映射,再按国家并行发送请求到服务端;服务端通过Razor生成强类型HTML,借助IronPDF库转换为PDF后返回主应用。

核心代码

主应用代码

public async Task PublishDocuments(List<string> countries)
{
    var requests = new Dictionary<string, PublishRequest>();

    foreach (var country in countries)
    {
        // 同步准备数据并添加到requests字典
    }

    using var httpClientHandler = new HttpClientHandler();
    httpClientHandler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;

    using var httpClient = new HttpClient(httpClientHandler);
    httpClient.Timeout = TimeSpan.FromMinutes(15);

    var publishTasks = requests.Select(x => GetAndSaveFile(x.Value, x.Key, httpClient));
    await Task.WhenAll(publishTasks);

    // 后续数据库操作
}

private async Task GetAndSaveFile(PublishRequest request, string fullFilePath, HttpClient httpClient)
{
    var res = await GetDocument(request, httpClient);
    File.WriteAllBytes(fullFilePath, res);
}

private async Task<byte[]> GetDocument(PublishRequest request, HttpClient httpClient)
{
    var serializedRequest = JsonConvert.SerializeObject(request);

    var content = new StringContent(serializedRequest, System.Text.Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync("https://localhost:7000/publish", content);

    return await response.Content.ReadAsByteArrayAsync();
}

服务端代码

app.MapPost("/publish", async (PublishRequest request, HttpContext context) =>
{
    // 使用Razor生成强类型HTML,依赖IronPDF转PDF
    var htmlContent = await RenderRazorViewToStringAsync(context, "Document", request);
    var renderer = new ChromePdfRenderer();

    renderer.RenderingOptions.EnableJavaScript = true;
    renderer.RenderingOptions.WaitFor.JavaScript(500);

    var pdf = renderer.RenderHtmlAsPdf(htmlContent);

    context.Response.ContentType = "application/pdf";
    context.Response.StatusCode = 200;
    context.Response.Headers.Add("Content-Disposition", "attachment; filename=generated.pdf");
    await context.Response.Body.WriteAsync(pdf.BinaryData);
}).WithRequestTimeout(TimeSpan.FromMinutes(10));

异常信息

请求数量超过阈值时出现两类错误:

  1. PublishDocuments方法中抛出:

System.Net.Http.HttpRequestException: 'An error occurred while sending the request.'

at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()

at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)

at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()

at PublishService.d__22.MoveNext() in...

内部异常:

The underlying connection was closed: The connection was closed unexpectedly.

at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)

at System.Net.Http.HttpClientHandler.GetResponseCallback(IAsyncResult ar)

  1. GetDocument方法中抛出:

Error while copying content to a stream.

at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()

at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)

at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()

at PublishService.d__22.MoveNext() in ...

内部异常:

Unable to read data from the transport connection: The connection was closed.

at System.Net.ConnectStream.IOError(Exception exception, Boolean willThrow)

at System.Net.ConnectStream.InternalWrite(Boolean async, Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)

at System.Net.ConnectStream.BeginWrite(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)

at System.Threading.Tasks.TaskFactory1.FromAsyncImpl[TArg1,TArg2,TArg3](Func6 beginMethod, Func2 endFunction, Action1 endAction, TArg1 arg1, TArg2 arg2, TArg3 arg3, Object state, TaskCreationOptions creationOptions)

at System.Net.Http.ByteArrayContent.SerializeToStreamAsync(Stream stream, TransportContext context)

at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context)

已尝试无效方案

  • 将HttpClient实例超时设置为15分钟;
  • 曾设置System.Net.ServicePointManager.Expect100Continue = false(无效后移除);
  • 复用HttpClient实例而非每个请求创建新实例;
  • 服务端API方法设置10分钟请求超时。

可行解决办法

1. 限制并行请求数量

.NET 4.8中ServicePointManager.DefaultConnectionLimit默认值为2,批量并行请求会耗尽连接池,引发连接异常。可以:

  • 全局调整连接限制(比如在Global.asax的Application_Start中):
    System.Net.ServicePointManager.DefaultConnectionLimit = 20; // 根据机器性能调整
    
  • 使用SemaphoreSlim控制并发请求数,避免瞬间压垮服务端:
    // 在PublishDocuments方法中修改任务创建逻辑
    var semaphore = new SemaphoreSlim(10); // 限制同时10个请求
    var publishTasks = requests.Select(async x =>
    {
        await semaphore.WaitAsync();
        try
        {
            await GetAndSaveFile(x.Value, x.Key, httpClient);
        }
        finally
        {
            semaphore.Release();
        }
    });
    await Task.WhenAll(publishTasks);
    

2. 优化服务端资源占用

IronPDF的ChromePdfRenderer每次实例化会启动Chrome进程,批量请求会消耗大量CPU、内存:

  • 复用全局单例的ChromePdfRenderer实例,减少资源开销:
    // 在服务端启动类中定义全局实例
    private static readonly ChromePdfRenderer _renderer = new ChromePdfRenderer();
    static YourStartupClass()
    {
        _renderer.RenderingOptions.EnableJavaScript = true;
        _renderer.RenderingOptions.WaitFor.JavaScript(500);
    }
    
    // 在接口中复用该实例
    app.MapPost("/publish", async (PublishRequest request, HttpContext context) =>
    {
        var htmlContent = await RenderRazorViewToStringAsync(context, "Document", request);
        var pdf = _renderer.RenderHtmlAsPdf(htmlContent);
    
        // 响应写入逻辑
    }).WithRequestTimeout(TimeSpan.FromMinutes(10));
    

3. 规范服务端响应处理

替换直接写入Response.Body的方式,使用ASP.NET Core内置的Results.File返回,确保响应完整发送:

app.MapPost("/publish", async (PublishRequest request, HttpContext context) =>
{
    var htmlContent = await RenderRazorViewToStringAsync(context, "Document", request);
    var renderer = new ChromePdfRenderer();
    renderer.RenderingOptions.EnableJavaScript = true;
    renderer.RenderingOptions.WaitFor.JavaScript(500);
    var pdf = renderer.RenderHtmlAsPdf(htmlContent);

    return Results.File(pdf.BinaryData, "application/pdf", "generated.pdf");
}).WithRequestTimeout(TimeSpan.FromMinutes(10));

4. 启用HTTP/2优化连接

.NET 4.8的HttpClient支持HTTP/2,启用后可提升并发请求处理效率:

// 在主应用创建HttpClientHandler时配置
var httpClientHandler = new HttpClientHandler();
httpClientHandler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
httpClientHandler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13;
httpClientHandler.UseHttp2 = true;

5. 添加错误重试机制

针对偶发的连接异常,使用Polly库实现重试逻辑:

// 安装Polly NuGet包后配置重试策略
var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .Or<IOException>()
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

// 在GetDocument方法中应用策略
private async Task<byte[]> GetDocument(PublishRequest request, HttpClient httpClient)
{
    return await retryPolicy.ExecuteAsync(async () =>
    {
        var serializedRequest = JsonConvert.SerializeObject(request);
        var content = new StringContent(serializedRequest, System.Text.Encoding.UTF8, "application/json");
        var response = await httpClient.PostAsync("https://localhost:7000/publish", content);
        response.EnsureSuccessStatusCode(); // 触发重试条件
        return await response.Content.ReadAsByteArrayAsync();
    });
}

内容的提问来源于stack exchange,提问作者dzenesiz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 04:35:54