why writing to HttpContext.Response "short-circuit" MVC pipeline

Viewed 43

below is the code:

public class Startup
{
   // ...
   public void Configure(IApplicationBuilder app)
   {
      app.UseRouting();

      app.Use(async (context, next) =>
      {
         await context.Response.WriteAsync("write sth first\n");
         await next();
      });

      app.UseEndpoints(endpoints =>
      {
         endpoints.MapControllers();
      });

   }
}

I got only "write sth first" response, the content generated by controller's action method is not appended to the response. However, I put a debugger on the action method of the controller, the debugger was hitting, which means the MVC pipeline was still running(which is not really short circuit) then why the response from action method is not in the response?

What's confusing me more is, if I change the code to:

public class Startup
{
   // ...
   public void Configure(IApplicationBuilder app)
   {
      app.UseRouting();

      app.Use(async (context, next) =>
      {
         await context.Response.WriteAsync("write sth first\n");
         await next();
      });

      app.UseEndpoints(endpoints =>
      {
         endpoints.MapGet("/", async context => {   
            await context.Response.WriteAsync("Hello World!");
         });
      });

   }
}

then I got a response that is "write sth first\n "Hello World!", so why this time the response from the endpoint is appended to the Response?

Is anywhere in endpoints.MapControllers() that check the HttpContext.Response like if there is content then abort the remaining process? where is the source code for this part?

3 Answers

Here's the error when I tried

System.InvalidOperationException: Headers are read-only, response has already started.

So it has nothing to do with middleware or MVC pipelines. The controller needs to write HTTP headers but it cannot because the response has already started.

Seeing as other middleware might also need to write to the Response, you shouldn't start writing to it yourself. You could try attaching to the OnStarting event instead. That way you won't start the Response yourself (Pseudocode from memory):

app.Use(async (context, next) =>
{
    context.Response.OnStarting(async () =>
    {
        await context.Response.WriteAsync("write sth first\n");
    });

    await next();
}

why this time the response from the endpoint is appended to the Response

That is because the endpoint middleware is called after your custom middleware. So first it calls your middleware, then it calls the endpoint middleware. Middleware order matters.

Based on tia's answer,I tried to replaced the default response body and the Response.HasStarted attribute is false,you could see the page was rendered with starttime and end time,context.Response.WriteAsync will not short circuit the pipeline :

app.Use(async (context, next) =>
            {
                var response = context.Response;
                var responseOriginalBody = response.Body;
                using var memStream = new MemoryStream();
                response.Body = memStream;

                await context.Response.WriteAsync(string.Format("<a>starttime:{0}</a>", DateTime.Now.ToString("yyyy/MM/dd/HH:mm:ss.fff")));
                var start = context.Response.HasStarted;                
                await next.Invoke();
                await context.Response.WriteAsync(string.Format("<a>endtime:{0}</a>", DateTime.UtcNow.ToString("yyyy/MM/dd/HH:mm:ss.fff")));
                
                memStream.Position = 0;
                await memStream.CopyToAsync(responseOriginalBody);
                response.Body = responseOriginalBody;
            });

Result: enter image description here

Related