.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();
问题原因与解决方案
核心原因
- 响应压缩导致Chunked传输:生产环境启用了
UseResponseCompression,且IIS默认开启动态压缩。当响应被压缩时,ASP.NET Core会采用Chunked编码传输数据——这种模式下无法提前确定压缩后的响应长度,因此不会设置ContentLength,导致context.Response.ContentLength为NULL。而开发环境默认不启用响应压缩,响应直接发送,ContentLength能正常获取。 - 中间件注册顺序:追踪中间件注册在
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); } }
额外优化建议
- 调整中间件顺序:若需要记录未压缩的原始响应长度,可将
UseUserTracking注册到UseResponseCompression之前;若要记录实际发送给客户端的压缩后长度,仍需使用上述缓冲方案。 - 避免内存溢出:如果存在大文件响应场景,可使用临时文件流替代
MemoryStream,防止内存占用过高。
内容的提问来源于stack exchange,提问作者Gillardo
相关产品推荐
相关产品推荐

