3

Ciao,

I'm developing a series of microservices with aspnet core. I want to return a custom http header on 500 responses.

I tried to create a custom ASP.NET Core Middleware that update context.Response.Headers property but it works only when the response is 200.

This is my custom middleware:

namespace Organizzazione.Progetto
{
    public class MyCustomMiddleware
    {
        private readonly RequestDelegate _next;

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

        public async Task InvokeAsync(HttpContext context)
        {
            context.Response.Headers.Add("X-Correlation-Id", Guid.NewGuid());
            await _next.Invoke(context);
            return;
        }
    }
}

This is my configure method:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMiddleware<MyCustomMiddleware>();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseMvc();
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API");
    });
}

How can I return my custom header on a 500 response caused by unhandled exception (or possibly on all the responses)?

Thank you a lot

2
  • What's causing the 500 response? Uncaught exception? If so you could write a global exception handling middleware that modifies the response headers. Commented Oct 19, 2018 at 14:16
  • Hi Chris, the 500 response is caused my uncaught exception. I update the question to clarify this concept. Thank you Commented Oct 19, 2018 at 14:36

2 Answers 2

5

You have to subscribe on httpContext.Response.OnStarting

public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;

    public CorrelationIdMiddleware(RequestDelegate next)
    {
        this._next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        httpContext.Response.OnStarting((Func<Task>)(() =>
        {
            httpContext.Response.Headers.Add("X-Correlation-Id", Guid.NewGuid().ToString());
            return Task.CompletedTask;
        }));
        try
        {
            await this._next(httpContext);

        }
        catch (Exception)
        {
            //add additional exception handling logic here 
            //...
            httpContext.Response.StatusCode = 500;
        }

    }
}

And register it your Starup

 app.UseMiddleware(typeof(CorrelationIdMiddleware));
Sign up to request clarification or add additional context in comments.

Comments

1

You can use a global filter :

public class CorrelationIdFilter : IActionFilter
    {
        /// <summary>
        /// Called after the action executes, before the action result.
        /// </summary>
        /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext" />.</param>
        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        /// <summary>
        /// Called before the action executes, after model binding is complete.
        /// </summary>
        /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public void OnActionExecuting(ActionExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add("X-Correlation-Id", Guid.NewGuid().ToString());
        }
    }

If you do the same but only on exception, use a IExceptionFilter:

public class CorrelationIdFilter : IExceptionFilter, IAsyncExceptionFilter
{
    private readonly ILogger<CorrelationIdFilter> _logger;

    /// <summary>
    /// Initialize a new instance of <see cref="CorrelationIdFilter"/>
    /// </summary>
    /// <param name="logger">A logger</param>
    public CorrelationIdFilter(ILogger<CorrelationIdFilter> logger)
    {
        _logger = logger;
    }
    /// <summary>
    /// Called after an action has thrown an <see cref="T:System.Exception" />.
    /// </summary>
    /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param>
    /// <returns>
    /// A <see cref="T:System.Threading.Tasks.Task" /> that on completion indicates the filter has executed.
    /// </returns>
    public Task OnExceptionAsync(ExceptionContext context)
    {
        Process(context);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Called after an action has thrown an <see cref="T:System.Exception" />.
    /// </summary>
    /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param>
    public void OnException(ExceptionContext context)
    {
        Process(context);
    }

    private void Process(ExceptionContext context)
    {
        var e = context.Exception;
        _logger.LogError(e, e.Message);

        context.HttpContext.Response.Headers.Add("X-Correlation-Id", Guid.NewGuid().ToString());

        if (e is InvalidOperationException)
        {
            context.Result = WriteError(HttpStatusCode.BadRequest, e);
        }
        else if (e.GetType().Namespace == "Microsoft.EntityFrameworkCore")
        {
            context.Result = WriteError(HttpStatusCode.BadRequest, e);
        }
        else
        {
            context.Result = WriteError(HttpStatusCode.InternalServerError, e);
        }                

    }


    private IActionResult WriteError(HttpStatusCode statusCode, Exception e)
    {
        var result = new ApiErrorResult(e.Message, e)
        {
            StatusCode = (int)statusCode,               
        };

        return result;
    }

}

/// <summary>
/// Api error result
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.ObjectResult" />
public class ApiErrorResult : ObjectResult
{
    private readonly string _reasonPhrase;

    /// <summary>
    /// Initializes a new instance of the <see cref="ApiErrorResult"/> class.
    /// </summary>
    /// <param name="reasonPhrase">The reason phrase.</param>
    /// <param name="value">The value.</param>
    public ApiErrorResult(string reasonPhrase, object value) : base(value)
    {
        _reasonPhrase = reasonPhrase;
    }

    /// <inheritdoc />
    public override async Task ExecuteResultAsync(ActionContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var reasonPhrase = _reasonPhrase;
        reasonPhrase = reasonPhrase.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)[0];
        context.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = reasonPhrase;
        await base.ExecuteResultAsync(context);
    }
}

And register your filter with :

services.AddMvc(configure =>
            {
                var filters = configure.Filters;
                filters.Add<CorrelationIdFilter >();
            })

4 Comments

Hi agua, thank you for your response, I've implemented your solution but on exception I obtain responses without CorrelationId.
Is the exception throw by MVC ?
Hi, Yes. I've added one new Exception() at begin of my action.
My bad, you need to send an ObjectResult. I updated the response

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.