65

I want to add a processing time middleware to my ASP.NET Core WebApi like this

public class ProcessingTimeMiddleware  
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        var watch = new Stopwatch();
        watch.Start();

        await _next(context);

        context.Response.Headers.Add("X-Processing-Time-Milliseconds", new[] { watch.ElapsedMilliseconds.ToString() });
    }
}

But doing this throws an exception saying

 System.InvalidOperationException: Headers are readonly, reponse has already started.

How can I add headers to the response?

6 Answers 6

90

Never mind, the code is here

    public async Task Invoke(HttpContext context)
    {
        var watch = new Stopwatch();
        watch.Start();

        //To add Headers AFTER everything you need to do this
        context.Response.OnStarting(state => {
            var httpContext = (HttpContext)state;
            httpContext.Response.Headers.Add("X-Response-Time-Milliseconds", new[] { watch.ElapsedMilliseconds.ToString() });

            return Task.CompletedTask;
        }, context);

        await _next(context);
    }
Sign up to request clarification or add additional context in comments.

8 Comments

I would add also that await _next(context) should be the last statement (after context.Response.Headers.Add). More semantically correct.
Is there a .NET 4 equivalent or work around for this? Still got some customers of Server 2003 :(
Is there a reason you used the state overload on OnStarting?
@MarkLopez because you need to convert the object (state) to HttpContext. Other overload doesnt have this, but see what you mean, could just use context variable instead.
I mean, why not just let the closure capture the context variable to avoid the need to cast? Although, I later realized that the state is given in the API to not require developers to use a Lambda.
|
19

Response headers can't be set after anything has been written to the response body.Once you pass the request to next middleware and it writes to the Response, then the Middleware can't set the Response headers again.

However, there is a solution available using a Callback method.

Microsoft.AspNetCore.Http.HttpResponse defines the OnStarting Method, which Adds a delegate to be invoked just before response headers will be sent to the client. You can think this method as a callback method that will be called right before writing to the response starts.

public class ResponseTimeMiddleware
    {
        private const string RESPONSE_HEADER_RESPONSE_TIME = "X-Response-Time-ms";

        private readonly RequestDelegate _next;

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

        public Task InvokeAsync(HttpContext context)
        {
            var watch = new Stopwatch();
            watch.Start();

            context.Response.OnStarting(() => 
            {
                watch.Stop();
                var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
                context.Response.Headers[RESPONSE_HEADER_RESPONSE_TIME] =  responseTimeForCompleteRequest.ToString(); 
                return Task.CompletedTask;
            });

            // Call the next delegate/middleware in the pipeline
            return this._next(context);
        }
    }

Comments

14

Alternatively you can also add a middleware directly in the Startup.cs Configure method.

app.Use(next => async context =>
{
    var stopWatch = new Stopwatch();
    stopWatch.Start();

    context.Response.OnStarting(() =>
    {
        stopWatch.Stop();
        context.Response.Headers.Add("X-ResponseTime-Ms", stopWatch.ElapsedMilliseconds.ToString());
        return Task.CompletedTask;
    });

    await next(context);
});

app.UseMvc();

2 Comments

This will give me OnStarting cannot be set because the response has already started
@cederlof You need to put this middleware before any other middleware.
5

On a related note, without answering your question as such, there is now a Server-Timing specification, a standard header to provide durations, amongst other metrics. This should allow you to use

Server-Timing: processingTime;dur=12ms

You can find the specification at https://www.w3.org/TR/server-timing/

Comments

4

Using an overload of OnStarting method:

public async Task Invoke(HttpContext context)
{
    var watch = new Stopwatch();

    context.Response.OnStarting(() =>
    {
        watch.Stop();

        context
              .Response
              .Headers
              .Add("X-Processing-Time-Milliseconds",
                        new[] { watch.ElapsedMilliseconds.ToString() });

        return Task.CompletedTask;
    });

    watch.Start();

    await _next(context); 
}

Comments

0

In your example headers already sent, when execution reaches context.Response.Headers.Add(...) statement.

You can try:

public async Task Invoke(HttpContext context)
{
    var watch = new Stopwatch();
    context.Response.OnSendingHeaders(x =>
    {
        watch.Stop();
        context.Response.Headers.Add("X-Processing-Time-Milliseconds", new[] { watch.ElapsedMilliseconds.ToString() });
    }, null);

    watch.Start();
    await _next(context);
    watch.Stop();
}

1 Comment

From which package is OnSendingHeaders ?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.