3

I recently upgraded an API to .NET Core 3, and as far as I can tell, this bit stopped working after the upgrade.

I want to log exceptions AND the json payload that was passed in via the request body. This ExceptionLogger is plugged in as a filter to handle errors globally:

public class ExceptionLogger
{
    private readonly IServiceScopeFactory _factory;

    public ExceptionLogger(IServiceScopeFactory factory)
    {
        _factory = factory;
    }   

    public async Task<Guid?> LogException(Exception ex)
    {
        using var scope = _factory.CreateScope();
        var accessor = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
        var displayUrl = accessor?.HttpContext?.Request?.GetDisplayUrl();
        var payload = await RetrievePayload(accessor, displayUrl);

        // ... code proceeds to log the exception, including the payload.
    }

    private async Task<string> RetrievePayload(IHttpContextAccessor accessor, string displayUrl)
    {
        accessor.HttpContext.Request.EnableBuffering();
        accessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);
        using var sr = new System.IO.StreamReader(accessor.HttpContext.Request.Body);

        return await sr.ReadToEndAsync(); // The request body is always empty now....!
    }
}

The accessor has querystring and form parameters, and even the accessor.HttpContext.Request.ContentLength has a value indicating the payload was there. However, the Request.Body stream is empty, even after I reset the cursor position.

How can I get the request body at this point?

Jakob Gade
  • 12,319
  • 15
  • 70
  • 118

1 Answers1

4

Here is the thing with .net core 3.0. If you try to read that body multiple times before configuring it for this it will succeed at first read and will fail for the consequent reads. So you need a middleware to configure that body for multiple reads. As mentioned here there was a work around for that marking request with request.EnableRewind() and as mentioned here you would need to change any calls to EnableRewind to EnableBuffering. So if we return your case your error handler should be like below :

    public class ErrorLoggingMiddleware
{
    private readonly RequestDelegate next;

    public ErrorLoggingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            context.Request.EnableBuffering(); // this part is crucial
            await this.next(context);
        }
        catch (Exception e)
        {

            var stream = context.Request.Body;
            stream.Seek(0, SeekOrigin.Begin);
            using var reader = new StreamReader(stream);
            var bodyString =  await reader.ReadToEndAsync();


            // log the error
        }
    }
}

Or you can separate your middleware into two pieces first for the rewind thing as mentioned in the first link. Because it has to run before the first read to the body. (Need to be registered first in StartUp.cs Configure) Then you can register your error logger middlerware or you can also use filter because we already enabled body for multiple reads.

Eldar
  • 9,781
  • 2
  • 10
  • 35
  • Excellent, thank you. It works. I was playing around with request.EnableReview in a middleware class, but couldn't get it to work. :) – Jakob Gade Nov 15 '19 at 21:12