中间件清空响应内容

Middleware clearing response content

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

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());
            }
        }
    }
}

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

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

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

我还有一个服务可以直接与 API 通信并修改 StatusCode,这取决于它从 API.

得到的响应

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

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

但是如果没有错误或者none的中间件短路了请求,如果一切顺利但只有StatusCode改变了,例如如果我的服务returns 401 未经授权的错误代码如下:

var response = await client.SendAsync(request);

//_contextAccessor.HttpContext.Response.StatusCode = (int)response.StatusCode;
_contextAccessor.HttpContext.Response.StatusCode = 401;

然后它将以来自成功请求的原始 html 代码结束, 来自错误处理程序中间件的 html 代码,如中所示下图:

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

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

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

System.InvalidOperationException: The response cannot be cleared, it has already started sending.

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

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

System.NotSupportedException: Specified method is not supported.

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

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

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);
    }
    
}

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

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