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

.NET 8生产环境Response.ContentLength为NULL,开发环境正常

问题描述

用C#和.NET 8编写了一个用户追踪中间件,用于记录用户信息、访问端点、请求大小及响应大小。本地执行dotnet run时功能正常,但部署到Windows Server 2022的IIS服务器后,Response.ContentLength始终为NULL,导致数据库中responseLength字段为空。

中间件代码

/// <summary>
/// Configure our httpRequest to be buffered so we can read it multiple times ourselves
/// </summary>
public class UserTrackingMiddleware
{
    private readonly RequestDelegate _next;

    public UserTrackingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        //  get host from the request and check if it's in the enumeration of allowed hosts
        var url = context.Request.GetDisplayUrl();
        var querystring = context.Request.QueryString.ToString();
        var endpoint = context.GetEndpoint();

        if (!string.IsNullOrEmpty(querystring))
        {
            url = url.Replace(querystring, string.Empty);
            querystring = querystring.Substring(1);
        }

        if (!string.IsNullOrEmpty(url))
        {
            var scope = context.RequestServices.CreateScope();
            var currentUser = scope.ServiceProvider.GetRequiredService<ICurrentUser>();
            var user = await currentUser.GetAsync();

            if (currentUser.IsAuthenticated)
            {
                var watch = new Stopwatch();
                watch.Start();

                var dataContext = scope.ServiceProvider.GetRequiredService<IDataContext>();

                // userAgent
                var userAgent = "unavailable";

                if (context.Request.Headers.TryGetValue("User-Agent", out var parsedUserAgent))
                    userAgent = parsedUserAgent.ToString();

                // contentLength
                long? requestLength = null;

                if (headerKeys.Contains("Content-Length", StringComparer.OrdinalIgnoreCase))
                    requestLength = long.Parse(context.Request.Headers["Content-Length"].ToString());

                // headers
                var httpHeaders = new Dictionary<string, string>();
                foreach (var header in headerKeys.OrderBy(e => e))
                    httpHeaders.Add(header, context.Request.Headers[header]);

                var log = new UserTracking
                {
                    UserId = user.Id,
                    Url = url,
                    QueryString = querystring,
                    RequestLength = requestLength,
                    UserAgent = userAgent,
                    Method = context.Request.Method.ToLower(),
                    Headers = JsonConvert.SerializeObject(httpHeaders)
                };

                dataContext.SetAdded(log);

                try
                {
                    await dataContext.SaveChangesAsync(context.RequestAborted);

                    // To add Headers AFTER everything you need to do this
                    context.Response.OnStarting(async () =>
                    {
                        watch.Stop();

                        var sqlBuilder = new SqlBuilder("UPDATE UserTracking SET ElapsedMilliseconds = {0}, ResponseLength = {1} WHERE Id = {2}", watch.ElapsedMilliseconds, context.Response.ContentLength, log.Id);
                        await dataContext.ExecuteSqlCommandAsync(sqlBuilder);
                    });
                }
                catch (Exception)
                {
                    // an error occurred here
                }
            }
        }

        await _next(context);
    }
}

本地配置(launchSettings.json)

"https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
}

中间件注册顺序(Program.cs)

// must be done before all other middleware, expect developer exception page
app.UseHsts(options =>
{
    options.MaxAge(120, 0, 1, 0);
    options.AllResponses();
    options.IncludeSubdomains();
});

// middleware
app.UseGlobalExceptionHandler();
app.UseOperationCancelledExceptionHandler();
app.UseResponseCaching();
app.UseResponseCompression();

// Content Security Policy (CSP)
app.UseSecurityPolicy();
app.UseCsp(options =>
{
    ...
});

// swagger
app.UseStaticFiles(new StaticFileOptions()
{
    ...
});

// app
app.UseStaticFiles(new StaticFileOptions()
{
    FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot", "app")),

    // accessing wwwroot inside folder contents
    RequestPath = new PathString(""),

    // add cache headers
    OnPrepareResponse = (context) =>
    {
        var headers = context.Context.Response.GetTypedHeaders();
        headers.CacheControl = new CacheControlHeaderValue()
        {
            Public = false,
            Private = true,
            MaxAge = TimeSpan.FromHours(24)
        };

        // these are also in the securityPolicyMiddleware
        headers.Set("Permissions-Policy", "accelerometer=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
        headers.Set("Referrer-Policy", "strict-origin-when-cross-origin");
        headers.Set("X-Content-Type-Options", "nosniff");
        headers.Set("X-Frame-Options", "DENY");
        headers.Set("X-Permitted-Cross-Domain-Policies", "none");
    }
});

// Enable middleware to serve generated Swagger as a JSON endpoint
app.UseSwagger(options =>
{
    ...
});

// Enable middleware to serve swagger-ui assets (HTML, JS, CSS etc.)
app.UseSwaggerUI(options =>
{
    ...
});

// routing
app.UseRouting();

// CORS - Configuring is with MvcExtensions
app.UseCors();

// comment this out and you get an error saying 
// InvalidOperationException: No authentication handler is configured to handle the scheme: Microsoft.AspNet.Identity.External
app.UseAuthentication();

// for authorization headers
app.UseAuthorization();

// antiforgery
app.UseAntiforgery();

// user tracking  <!--  HERE
app.UseUserTracking();
问题原因与解决方案

核心原因

  1. 响应压缩导致Chunked传输:生产环境启用了UseResponseCompression,且IIS默认开启动态压缩。当响应被压缩时,ASP.NET Core会采用Chunked编码传输数据——这种模式下无法提前确定压缩后的响应长度,因此不会设置ContentLength,导致context.Response.ContentLength为NULL。而开发环境默认不启用响应压缩,响应直接发送,ContentLength能正常获取。
  2. 中间件注册顺序:追踪中间件注册在UseResponseCompression之后,压缩中间件已修改响应流处理逻辑,进一步导致无法通过ContentLength获取长度。

解决方案:缓冲响应流获取实际长度

要准确获取响应长度,需将响应内容缓冲到内存流中,在响应完成后读取实际字节数。修改中间件的Invoke方法如下:

public async Task Invoke(HttpContext context)
{
    var url = context.Request.GetDisplayUrl();
    var querystring = context.Request.QueryString.ToString();
    var endpoint = context.GetEndpoint();

    if (!string.IsNullOrEmpty(querystring))
    {
        url = url.Replace(querystring, string.Empty);
        querystring = querystring.Substring(1);
    }

    if (!string.IsNullOrEmpty(url))
    {
        var scope = context.RequestServices.CreateScope();
        var currentUser = scope.ServiceProvider.GetRequiredService<ICurrentUser>();
        var user = await currentUser.GetAsync();

        if (currentUser.IsAuthenticated)
        {
            var watch = new Stopwatch();
            watch.Start();

            var dataContext = scope.ServiceProvider.GetRequiredService<IDataContext>();

            // 收集请求信息
            var userAgent = "unavailable";
            if (context.Request.Headers.TryGetValue("User-Agent", out var parsedUserAgent))
                userAgent = parsedUserAgent.ToString();

            long? requestLength = null;
            if (context.Request.Headers.ContainsKey("Content-Length"))
                requestLength = long.Parse(context.Request.Headers["Content-Length"].ToString());

            var httpHeaders = new Dictionary<string, string>();
            foreach (var header in headerKeys.OrderBy(e => e))
                httpHeaders.Add(header, context.Request.Headers[header]);

            var log = new UserTracking
            {
                UserId = user.Id,
                Url = url,
                QueryString = querystring,
                RequestLength = requestLength,
                UserAgent = userAgent,
                Method = context.Request.Method.ToLower(),
                Headers = JsonConvert.SerializeObject(httpHeaders)
            };

            dataContext.SetAdded(log);
            await dataContext.SaveChangesAsync(context.RequestAborted);

            // 保存原始响应流,替换为内存流缓冲
            var originalResponseBody = context.Response.Body;
            using var responseBuffer = new MemoryStream();
            context.Response.Body = responseBuffer;

            try
            {
                // 执行后续中间件
                await _next(context);

                // 重置内存流指针,获取实际响应长度
                responseBuffer.Seek(0, SeekOrigin.Begin);
                long responseLength = responseBuffer.Length;

                watch.Stop();

                // 更新追踪日志
                var sqlBuilder = new SqlBuilder(
                    "UPDATE UserTracking SET ElapsedMilliseconds = {0}, ResponseLength = {1} WHERE Id = {2}",
                    watch.ElapsedMilliseconds, 
                    responseLength, 
                    log.Id);
                await dataContext.ExecuteSqlCommandAsync(sqlBuilder);

                // 将缓冲的响应内容复制回原始流
                await responseBuffer.CopyToAsync(originalResponseBody);
            }
            finally
            {
                // 恢复原始响应流
                context.Response.Body = originalResponseBody;
            }
        }
        else
        {
            // 未认证用户直接执行后续中间件
            await _next(context);
        }
    }
    else
    {
        await _next(context);
    }
}

额外优化建议

  1. 调整中间件顺序:若需要记录未压缩的原始响应长度,可将UseUserTracking注册到UseResponseCompression之前;若要记录实际发送给客户端的压缩后长度,仍需使用上述缓冲方案。
  2. 避免内存溢出:如果存在大文件响应场景,可使用临时文件流替代MemoryStream,防止内存占用过高。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 20:24:53