Muhammad Azim
Muhammad Azim

Reputation: 329

Getting empty response on ASP.NET Core middleware on exception

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?

Upvotes: 10

Views: 17496

Answers (2)

RonC
RonC

Reputation: 33879

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

Upvotes: 10

Barr J
Barr J

Reputation: 10927

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

Upvotes: 1

Related Questions