在 ASP.NET 异常核心中间件上获得空响应

Getting empty response on ASP.NET Core middleware on exception

我正在尝试创建一个可以记录响应正文以及全局管理异常的中间件,我在这方面取得了成功。我的问题是我放入异常的自定义消息没有显示在响应中。

中间件代码 01:

public async Task Invoke(HttpContext context)
{
    context.Request.EnableRewind();
        
    var originalBodyStream = context.Response.Body;
    using (var responseBody = new MemoryStream())
    {
        try
        {
            context.Response.Body = responseBody;
            await next(context);

            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var response = await new StreamReader(context.Response.Body).ReadToEndAsync();
            context.Response.Body.Seek(0, SeekOrigin.Begin);

            // Process log
             var log = new LogMetadata();
             log.RequestMethod = context.Request.Method;
             log.RequestUri = context.Request.Path.ToString();
             log.ResponseStatusCode = context.Response.StatusCode;
             log.ResponseTimestamp = DateTime.Now;
             log.ResponseContentType = context.Response.ContentType;
             log.ResponseContent = response;
             // Keep Log to text file
             CustomLogger.WriteLog(log);

            await responseBody.CopyToAsync(originalBodyStream);
        }
        catch (Exception ex)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            var jsonObject = JsonConvert.SerializeObject(My Custom Model);
            await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
            return;
        }
    }
}

如果我这样写我的中间件,我的自定义异常工作正常但我无法记录我的响应正文。

中间件代码 02:

 public async Task Invoke(HttpContext context)
  {
    context.Request.EnableRewind();

    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var jsonObject = JsonConvert.SerializeObject(My Custom Model);
        await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
        return;
    }
}

我的控制器操作:

    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        throw new Exception("Exception Message");
    }

现在我想用我的中间件 01 显示我的异常消息,但它不起作用,但它在我的中间件 02 上起作用。

所以我的观察是读取上下文响应时出现问题。我的中间件 01 代码中是否遗漏了什么?

有没有更好的方法来满足我的目的,即记录响应正文以及全局管理异常?

有一篇精彩的文章详细解释了您的问题 - Using Middleware to trap Exceptions in Asp.Net Core

关于中间件你需要记住的是:

中间件会在启动期间添加到您的应用程序中,如上所示。您调用 Use... 方法的顺序很重要!中间件 "waterfalled" 直到全部执行完毕,或者一个停止执行。

传递给中间件的第一件事是请求委托。这是一个获取当前 HttpContext object 并执行它的委托。您的中间件在创建时保存它,并在 Invoke() 步骤中使用它。 Invoke() 是完成工作的地方。作为中间件的一部分,无论您想对 request/response 做什么,都可以在这里完成。中间件的一些其他用途可能是根据 header 授权请求或将 header 注入请求或响应

所以你做什么,你写了一个新的异常类型,和一个中间件处理程序来捕获你的异常:

新异常类型class:

public class HttpStatusCodeException : Exception
{
    public int StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(int statusCode)
    {
        this.StatusCode = statusCode;

    }
    public HttpStatusCodeException(int statusCode, string message) : base(message)

    {
        this.StatusCode = statusCode;
    }
    public HttpStatusCodeException(int statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(int statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())

    {
        this.ContentType = @"application/json";
    }
}

中间件处理程序:

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

    public HttpStatusCodeExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
        _logger = loggerFactory?.CreateLogger<HttpStatusCodeExceptionMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            if (context.Response.HasStarted)
            {
                _logger.LogWarning("The response has already started, the http status code middleware will not be executed.");
                throw;
            }

            context.Response.Clear();
            context.Response.StatusCode = ex.StatusCode;
            context.Response.ContentType = ex.ContentType;

            await context.Response.WriteAsync(ex.Message);

            return;
        }
    }
}

// Extension method used to add the middleware to the HTTP request pipeline.
public static class HttpStatusCodeExceptionMiddlewareExtensions
{
    public static IApplicationBuilder UseHttpStatusCodeExceptionMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HttpStatusCodeExceptionMiddleware>();
    }
}

然后使用你的新中间件:

  public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseHttpStatusCodeExceptionMiddleware();
    }
    else
    {
        app.UseHttpStatusCodeExceptionMiddleware();
        app.UseExceptionHandler();
    }

    app.UseStaticFiles();
    app.UseMvc();
}

最终用途很简单:

throw new HttpStatusCodeException(StatusCodes.Status400BadRequest, @"You sent bad stuff");

我想你的意思是这段代码没有将它的响应发送给客户端。

 catch (Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var jsonObject = JsonConvert.SerializeObject(My Custom Model);
        await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
        return;
    }

原因是 await context.Response.WriteAsync(jsonObject, Encoding.UTF8); 不是写入原始主体流,而是写入可搜索的内存流。所以在你写入它之后,你必须将它复制到原始流中。所以我相信代码应该是这样的:

 catch (Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var jsonObject = JsonConvert.SerializeObject(My Custom Model);
        await context.Response.WriteAsync(jsonObject, Encoding.UTF8);

        context.Response.Body.Seek(0, SeekOrigin.Begin);    //IMPORTANT!
        await responseBody.CopyToAsync(originalBodyStream); //IMPORTANT!
        return;
    }