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