10

I am trying to create a middleware that can log the response body as well as manage exception globally and I was succeeded about that. My problem is that the custom message that I put on exception it's not showing on the response.

Middleware Code 01:

public async Task Invoke(HttpContext context)
{
    context.Request.EnableRewind();
        
    var originalBodyStream = context.Response.Body;
    using (var responseBody = new MemoryStream())
    {
        try
        {
            context.Response.Body = responseBody;
            await next(context);

            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var response = await new StreamReader(context.Response.Body).ReadToEndAsync();
            context.Response.Body.Seek(0, SeekOrigin.Begin);

            // Process log
             var log = new LogMetadata();
             log.RequestMethod = context.Request.Method;
             log.RequestUri = context.Request.Path.ToString();
             log.ResponseStatusCode = context.Response.StatusCode;
             log.ResponseTimestamp = DateTime.Now;
             log.ResponseContentType = context.Response.ContentType;
             log.ResponseContent = response;
             // Keep Log to text file
             CustomLogger.WriteLog(log);

            await responseBody.CopyToAsync(originalBodyStream);
        }
        catch (Exception ex)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            var jsonObject = JsonConvert.SerializeObject(My Custom Model);
            await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
            return;
        }
    }
}

If I write my middleware like that, my custom exception is working fine but I unable to log my response body.

Middleware Code 02:

 public async Task Invoke(HttpContext context)
  {
    context.Request.EnableRewind();

    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var jsonObject = JsonConvert.SerializeObject(My Custom Model);
        await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
        return;
    }
}

My Controller Action :

    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        throw new Exception("Exception Message");
    }

Now I want to show my exception message with my middleware 01, but it doesn't work but its work on my middleware 02.

So my observation is the problem is occurring for reading the context response. Is there anything I have missed in my middleware 01 code?

Is there any better way to serve my purpose that log the response body as well as manage exception globally?

Dale K
  • 25,246
  • 15
  • 42
  • 71
Muhammad Azim
  • 329
  • 1
  • 2
  • 14
  • "my custom exception is working fine but I unable to log my response body." Please show the code that is attempting to log the response body. You have captured the response body in the `response` var but have not used that var in the currently stated code. – RonC Feb 26 '19 at 13:22
  • @RonC I am keeping my log in text file, I have updated my code, here i provide some information that i keep. – Muhammad Azim Feb 27 '19 at 04:50
  • than you for posting the additional code, this is helpful. It appears that your primary question is this "My problem is that the custom message that I put on exception it's not showing on the response." but I'm not sure what you mean. Can you restart this is a way that is more clear? – RonC Feb 27 '19 at 13:20
  • @RonC I have updated my code, hope you understand my problem. – Muhammad Azim Mar 03 '19 at 05:16

2 Answers2

10

I think what you are saying is that this code isn't sending it's response to the client.

 catch (Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var jsonObject = JsonConvert.SerializeObject(My Custom Model);
        await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
        return;
    }

The reason for this is that await context.Response.WriteAsync(jsonObject, Encoding.UTF8); isn't writing to the original body stream it's writing to the memory stream that is seekable. So after you write to it you have to copy it to the original stream. So I believe the code should look like this:

 catch (Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var jsonObject = JsonConvert.SerializeObject(My Custom Model);
        await context.Response.WriteAsync(jsonObject, Encoding.UTF8);

        context.Response.Body.Seek(0, SeekOrigin.Begin);    //IMPORTANT!
        await responseBody.CopyToAsync(originalBodyStream); //IMPORTANT!
        return;
    }
RonC
  • 31,330
  • 19
  • 94
  • 139
1

There is a wonderful article explaining in detail your problem - Using Middleware to trap Exceptions in Asp.Net Core.

What you need to remember about middleware is the following:

Middleware is added to your app during Startup, as you saw above. The order in which you call the Use... methods does matter! Middleware is "waterfalled" down through until either all have been executed, or one stops execution.

The first things passed to your middleware is a request delegate. This is a delegate that takes the current HttpContext object and executes it. Your middleware saves this off upon creation, and uses it in the Invoke() step. Invoke() is where the work is done. Whatever you want to do to the request/response as part of your middleware is done here. Some other usages for middleware might be to authorize a request based on a header or inject a header in to the request or response

So what you do, you write a new exception type, and a middleware handler to trap your exception:

New Exception type class:

public class HttpStatusCodeException : Exception
{
    public int StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(int statusCode)
    {
        this.StatusCode = statusCode;

    }
    public HttpStatusCodeException(int statusCode, string message) : base(message)

    {
        this.StatusCode = statusCode;
    }
    public HttpStatusCodeException(int statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(int statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())

    {
        this.ContentType = @"application/json";
    }
}

And the middlware handler:

public class HttpStatusCodeExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<HttpStatusCodeExceptionMiddleware> _logger;

    public HttpStatusCodeExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
        _logger = loggerFactory?.CreateLogger<HttpStatusCodeExceptionMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            if (context.Response.HasStarted)
            {
                _logger.LogWarning("The response has already started, the http status code middleware will not be executed.");
                throw;
            }

            context.Response.Clear();
            context.Response.StatusCode = ex.StatusCode;
            context.Response.ContentType = ex.ContentType;

            await context.Response.WriteAsync(ex.Message);

            return;
        }
    }
}

// Extension method used to add the middleware to the HTTP request pipeline.
public static class HttpStatusCodeExceptionMiddlewareExtensions
{
    public static IApplicationBuilder UseHttpStatusCodeExceptionMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HttpStatusCodeExceptionMiddleware>();
    }
}

Then use your new middleware:

  public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

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

    app.UseStaticFiles();
    app.UseMvc();
}

The end use is simple:

throw new HttpStatusCodeException(StatusCodes.Status400BadRequest, @"You sent bad stuff");
Barr J
  • 10,636
  • 1
  • 28
  • 46
  • I think the intent of the SO was to capture any exception. Are you saying then that the app should be wrap exceptions into `HttpStatusCodeException` or throw only that type? Also, why is `HttpStatusCodeExceptionMiddleware` asking for `ILoggerFactory` instead of just `ILogger`? – Frank Fajardo Feb 26 '19 at 06:35
  • @Barr J : Thanks for your response but i think this will not solve my problem. – Muhammad Azim Feb 26 '19 at 06:41
  • @FrankFajardo it should wrap the exceptions. As for the second questions, it's concepts, everyone with his view of the code :). – Barr J Feb 26 '19 at 06:48