Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
161 views
in Technique[技术] by (71.8m points)

c# - Middleware clearing response content

At the top of my Startup.Configure pipeline, I created a custom Middleware which handles errors of my asp net core application like this :

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

My errorhandling system relies mainly on the Response.StatusCode.

Basically, it just wraps the entire code procedure in a try/catch statement. If an error is catched, the StatusCode become 500.

I have some others middlewares, especially a MaintenanceMiddleware which can set the StatusCode to 503 instead, to specify that the API is under maintenance.

I also have a service which communicates directly with the API and modify the StatusCode too, depending on the response it got from the API.

So before sending the response to the client, if anything during the procedure modified the StatusCode to something else than a 2XX StatusCode, the errorhandler middleware catches the response and must erase its content to writes a simple html code which displays an error message.

To achieve that, I use the await context.Response.WriteAsync() method which writes the html code to the response. It works really great and fast if the application is under maintenance or if it catches an error in the code.

But if there is no error or if none of the middlewares short-circuit the request, if everything is going well but only the StatusCode is changed, for example if my service returns a 401 Unauthorized error code as following :

var response = await client.SendAsync(request);

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

Then it will ends up with the original html code from the successful request, and the html code from the errorhandler middleware as shown in the following illustration :

html content

In the Browser Console, my response will contains the 401 StatusCode as expected. But the page displays the whole html code as shown in the illustration.

So I was looking to a way to clear the content of the current response before my errorhandling middleware writes to the response, but nothing seems to work for my case.

I tried to add context.Response.Clear() before calling the await context.Response.WriteAsync(), but I get the following error :

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

So instead of it I tried to reset the response body as following : context.Response.Body = new MemoryStream(), but this no longer writes the errorhandling code to my response, and does not reset the content at all. Only the original code from the request is displayed.

I also tried with context.Response.Body.SetLength(0) but this gave me the following error :

System.NotSupportedException: Specified method is not supported.

I'm running out of solutions actually. How can I reset the Response content before writing to it again ?

question from:https://stackoverflow.com/questions/65901584/middleware-clearing-response-content

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Before you Invoke the remainder of the pipeline via the wrapped RequestDelegate, replace the Response stream with a temporary MemoryStream. This will cause all of the inner middleware (those registered later in the pipeline) to write into your temporary buffer. Afterward, use whatever logic you need to determine if you should output a custom response or use the original one. If the latter, copy the temporary buffer back into the original response stream; otherwise write your custom response.

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

I've elided some of your code above and restructured it a bit for demonstration purposes. You can reorganize it to best suit your needs--just be sure to reset the Response.Body before you begin writing to it!

In order for this to work you must register your middleware early in the pipeline (i.e. in Startup.Configure). Otherwise an outer middleware (one registered before it) could start writing to the response prior to your middleware being invoked, which is precisely what we need to avoid.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...