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

.NET Core异常处理器响应头丢失及响应日志中间件问题排查

问题:IExceptionHandler添加响应头被Kestrel移除,且ResponseLoggingMiddleware导致OnStarting添加头失效

问题描述

  • 在IExceptionHandler实现中添加自定义响应头(X-Client-IP、trace-id、interaction-id),Kestrel会将这些头全部移除。
  • 改用context.Response.OnStarting回调添加头后,引入自定义响应日志中间件ResponseLoggingMiddleware1,该添加头的方法也失效。

自定义异常处理器代码

internal class CustomExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception,
        CancellationToken cancellationToken)
    {
        var traceId = Guid.NewGuid().ToString();
        var interactionId = Guid.NewGuid().ToString();
        var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "Unknown";

        context.Response.Clear();
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        context.Response.ContentType = "application/json";
        
        context.Response.Headers["X-Client-IP"] = clientIp;
        context.Response.Headers["trace-id"] = traceId;
        context.Response.Headers["interaction-id"] = interactionId;

        var errorResponse = new
        {
            StatusCode = context.Response.StatusCode,
            Message = "An unexpected error occurred.",
            TraceId = traceId,
            InteractionId = interactionId
        };

        await context.Response.WriteAsync(JsonSerializer.Serialize(errorResponse), cancellationToken);
        return false;
    }
}

失效的OnStarting添加头代码

context.Response.OnStarting(() =>
{
    context.Response.Headers.Append("X-Client-IP", clientIp);
    context.Response.Headers.Append("trace-id", traceId);
    context.Response.Headers.Append("interaction-id", interactionId);
    return Task.CompletedTask;
});

原响应日志中间件代码

public class ResponseLoggingMiddleware1
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ResponseLoggingMiddleware1> _logger;

    public ResponseLoggingMiddleware1(RequestDelegate next, ILogger<ResponseLoggingMiddleware1> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Create a memory stream to capture the response body
        var originalBodyStream = context.Response.Body;

        using (var memoryStream = new MemoryStream())
        {
            context.Response.Body = memoryStream;

            // Continue processing the request
            await _next(context);

            // Log the response information
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var responseBody = await new StreamReader(context.Response.Body).ReadToEndAsync();
            _logger.LogInformation($"Response Status: {context.Response.StatusCode}");
            _logger.LogInformation($"Response Body: {responseBody}");

            // Reset the body stream position and copy it to the original body stream
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            await memoryStream.CopyToAsync(originalBodyStream);
        }
    }
}

问题根源

问题出在原响应日志中间件ResponseLoggingMiddleware1:

  • 用using包裹MemoryStream,块结束后流会自动关闭。
  • OnStarting回调执行时,尝试操作已关闭的响应流,导致添加头失效;同时IExceptionHandler中直接添加的头,也会因为流提前关闭,被Kestrel判定为无效而移除。

修正方案

修改响应日志中间件,确保操作完成后恢复原始响应流,避免流提前关闭:

修正后的响应日志中间件代码

public class ResponseLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ResponseLoggingMiddleware> _logger;

    public ResponseLoggingMiddleware(RequestDelegate next, ILogger<ResponseLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Preserve the original response body stream
        var originalBodyStream = context.Response.Body;

        try
        {
            // Use a memory stream to capture the response
            using (var memoryStream = new MemoryStream())
            {
                context.Response.Body = memoryStream;

                // Call the next middleware in the pipeline
                await _next(context);

                // Read the response stream and log it
                memoryStream.Seek(0, SeekOrigin.Begin);
                var responseBody = await new StreamReader(memoryStream).ReadToEndAsync();

                // Log response details
                _logger.LogInformation($"Response Status: {context.Response.StatusCode}");
                _logger.LogInformation($"Response Body: {responseBody}");

                // Copy the response body back to the original stream
                memoryStream.Seek(0, SeekOrigin.Begin);
                await memoryStream.CopyToAsync(originalBodyStream);
            }
        }
        finally
        {
            // Restore the original response body stream
            context.Response.Body = originalBodyStream;
        }
    }
}

关键修正点

  1. 新增try-finally块,确保无论是否异常,都能恢复原始响应流。
  2. 直接操作memoryStream读取和复制内容,避免流状态不一致的问题。

内容的提问来源于stack exchange,提问作者A.kadir olmez

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 15:23:16