中间件清除响应内容

M. *_*Ozn 5 c# httpresponse asp.net-core-middleware asp.net-core-3.1

在管道的顶部Startup.Configure,我创建了一个自定义中间件,它处理我的 asp net core 应用程序的错误,如下所示:

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception)
        {
            if (context.Response.StatusCode != (int)HttpStatusCode.ServiceUnavailable && context.Response.StatusCode != (int)HttpStatusCode.InternalServerError)
            {
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            }
        }
        finally
        {
            if (context.Response.StatusCode / 100 != (int)HttpStatusCode.OK / 100)
            {
                string message = ((HttpStatusCode)context.Response.StatusCode) switch
                {
                    HttpStatusCode.ServiceUnavailable => "The application is currently on maintenance. Please try again later.",
                    HttpStatusCode.Unauthorized => "You are not authorized to view this content.",
                    HttpStatusCode.NotFound => "The content you asked for doesn't exist.",
                    HttpStatusCode.InternalServerError => "There was an internal server error. Please try again in a few minutes.",
                    _ => "Something went wrong. Please verify your request or try again in a few minutes.",
                };

                StringBuilder sb = new StringBuilder();
                sb.Append($"<html><head><title>Error {context.Response.StatusCode}</title></head><body>");
                sb.AppendLine();
                sb.Append($"<h1>Error {context.Response.StatusCode} : {(HttpStatusCode)context.Response.StatusCode}</h1>");
                sb.AppendLine();
                sb.Append($"<p>{message}</p>");
                sb.AppendLine();
                sb.Append("<br /><input style='display: block;' type='button' value='Go back to the previous page' onclick='history.back()'><br /><br />");
                sb.AppendLine();
                if (context.Response.StatusCode != (int)HttpStatusCode.Unauthorized && context.Response.StatusCode != (int)HttpStatusCode.NotFound)
                {
                    int delay = 5; //Minutes

                    sb.Append($"<i>This page will be automatically reloaded every {delay} minutes</i>");
                    sb.AppendLine();
                    sb.Append("<script>setTimeout(function(){window.location.reload(1);}, " + (delay * 1000 * 60) + ");</script>");
                    sb.AppendLine();
                }
                sb.Append("</body></html>");

                await context.Response.WriteAsync(sb.ToString());
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我的错误处理系统主要依赖于Response.StatusCode.

基本上,它只是将整个代码过程包装在一条try/catch语句中。如果捕获到错误,则StatusCode变为 500。

我还有一些其他中间件,尤其是MaintenanceMiddleware可以将 设为StatusCode503 的中间件,以指定 API 正在维护中。

我还有一个服务,它直接与 API 通信并修改它StatusCode,具体取决于从 API 获得的响应。

因此,在将响应发送到客户端之前,如果过程中有任何内容将 修改为StatusCode2XX StatusCode 以外的内容,则错误处理程序中间件会捕获响应,并且必须擦除其内容以写入显示错误消息的简单 html 代码。

为了实现这一点,我使用await context.Response.WriteAsync()将 html 代码写入响应的方法。如果应用程序正在维护或在代码中发现错误,它的工作效果会非常好且快速。

但是,如果没有错误或者没有中间件短路请求,如果一切顺利但只有更改StatusCode,例如,如果我的服务返回 401 Unauthorized 错误代码,如下所示:

var response = await client.SendAsync(request);

//_contextAccessor.HttpContext.Response.StatusCode = (int)response.StatusCode;
_contextAccessor.HttpContext.Response.StatusCode = 401;
Run Code Online (Sandbox Code Playgroud)

然后,它最终会得到来自成功请求的原始 html 代码,以及来自 errorhandler 中间件的 html 代码,如下图所示:

html内容

StatusCode在浏览器控制台中,我的响应将包含预期的401 。但页面显示了整个 html 代码,如图所示。

因此,我一直在寻找一种方法,在错误处理中间件写入响应之前清除当前响应的内容,但似乎没有任何方法适合我的情况。

context.Response.Clear()我尝试在调用之前添加await context.Response.WriteAsync(),但出现以下错误:

System.InvalidOperationException:无法清除响应,它已经开始发送。

因此,我尝试按以下方式重置响应正文,context.Response.Body = new MemoryStream()但这不再将错误处理代码写入我的响应,并且根本不重置内容。仅显示请求中的原始代码。

我也尝试过,context.Response.Body.SetLength(0)但这给了我以下错误:

System.NotSupportedException:不支持指定的方法。

实际上我已经没有解决方案了。在再次写入之前如何重置响应内容?

pin*_*x33 8

Invoke在通过包装的管道的其余部分之前RequestDelegate,将Response流替换为临时的 MemoryStream。这将导致所有内部中间件(稍后在管道中注册的中间件)写入临时缓冲区。然后,使用您需要的任何逻辑来确定是否应该输出自定义响应使用原始响应。如果是后者,则将临时缓冲区复制原始响应流中;否则写下您的自定义响应。

public async Task Invoke(HttpContext context){
    
    var writeCustomResponse = false;
    
    var originalResponse = context.Response.Body;
    var newResponse = new MemoryStream();
    
    // set to temporary buffer so wrapped middleware write to this
    context.Response.Body = newResponse;
    
    try {
         await _next(context);
    } 
    catch {
        // whatever
    } 
    finally {
    
        // set stream back and rewind temporary buffer
        context.Response.Body = originalResponse; 
        newResponse.Position = 0;

        writeCustomResponse = true; // your logic here
    }
    
    if (writeCustomResponse) {
        await context.Response.WriteAsync("whatever");
    } else {
        // copy temporary buffer into original
        await newResponse.CopyToAsync(context.Response.Body, context.RequestAborted);
    }
    
}
Run Code Online (Sandbox Code Playgroud)

为了演示目的,我删除了上面的一些代码并对其进行了一些重组。您可以重新组织它以最适合您的需要 - 只是请务必在Response.Body开始写入之前重置!

为了使其工作,您必须在管道中尽早注册您的中间件(即在 中Startup.Configure)。否则,外部中间件(在它之前注册的中间件)可能会在调用您的中间件之前开始写入响应,这正是我们需要避免的。