Logging the Body of HTTP Request and Response in ASP .NET Core

David Hadiprijanto | May 24th, 2017

Being able to log the raw data of an HTTP request and response in a web application is often quite useful – especially for troubleshooting.

With ASP .NET Core, it is relatively easy to inject our own code in the pipeline either through custom middleware or custom filter attributes which allow us to capture the information in the HTTP request and/or response and write them to our logging mechanism.

However, when it comes to capturing the body of an HTTP request and/or response, this is no trivial effort. In ASP .NET Core, the body is a stream – once you consume it (for logging, in this case), it’s gone, rendering the rest of the pipeline useless.

There is a “rewind” feature in stream objects, but the particular stream that ASP .NET Core uses – Microsoft.AspNetCore.Server.Kestrel.Internal.Http.FrameRequestStream – is not rewindable.

This blog is part of three post series.

  • In this first post, we will explore how to capture the body of an HTTP request for logging purposes and keep the stream body intact for the rest.
  • In the second post, we will do the same for HTTP responses.
  • In the third post, we will examine how to use the same basic technique to log only specific or targeted Controller API.

Logging the Body of an HTTP Request

In the following, we will use custom middleware to intercept the pipeline, so we can log the body of an HTTP request.

Let’s start with the skeleton:


using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace LogFullRequestResponse
{
    public class RequestLoggingMiddleware
    {   
        private readonly RequestDelegate _next;
        private readonly ILogger<RequestLoggingMiddleware> _logger;

        public RequestLoggingMiddleware(
		RequestDelegate next, 
		ILogger<RequestLoggingMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task Invoke(HttpContext context)
        {
            await _next.Invoke(context);                                
        } 
    }
}

This is pretty straight forward, standard custom ASP .NET Core middleware. The key part here is we are leveraging ASP .NET Core Dependency Injection mechanism to obtain the logger instance during instantiation of this middleware object and keep it for further usage.


 public RequestLoggingMiddleware(
		RequestDelegate next, 
		ILogger<RequestLoggingMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }

Next – in order to add our code to intercept the pipeline to do our logging, consider how the ASP .NET Core pipeline works:

ASP .Net Core Pipeline Diagram

What we want to do is to create a custom middleware, that does the following:

  1. Read the stream body of the request (the gray “ABC” box into the green “ABC” box)
  2. Log the content of the read stream body (the green “ABC” box into logger)
  3. Instantiate a new memory stream, write back the read stream body, and attach this memory stream into the request body (the green “ABC” box into the blue “ABC” box)
Custom middleware diagram

Now, let’s write the code!


public async Task Invoke(HttpContext context)
{
	injectedRequestStream = new MemoryStream();

    try
	{
		var requestLog = 
		$"REQUEST HttpMethod: {context.Request.Method}, Path: {context.Request.Path}";

		using (var bodyReader = new StreamReader(context.Request.Body))
		{
			var bodyAsText = bodyReader.ReadToEnd();
            if (string.IsNullOrWhiteSpace(bodyAsText) == false)
            {
				requestLog += $", Body : {bodyAsText}";
			}

			var bytesToWrite = Encoding.UTF8.GetBytes(bodyAsText);
			injectedRequestStream.Write(bytesToWrite, 0, bytesToWrite.Length);
			injectedRequestStream.Seek(0, SeekOrigin.Begin);
			context.Request.Body = injectedRequestStream;
		}

		_logger.LogTrace(requestLog);

		await _next.Invoke(context);                                
	}
    finally
    {
		injectedRequestStream.Dispose();
	}           
}    

Here are the breakdowns:

  1. First, we want to read the body of the HTTP request and log it.
    
     using (var bodyReader = new StreamReader(context.Request.Body))
                    {
                        var bodyAsText = bodyReader.ReadToEnd();
                        if (string.IsNullOrWhiteSpace(bodyAsText) == false)
                        {
                            requestLog += $", Body : {bodyAsText}";
                        }
    
                       // ... deleted
                    }
    
                    _logger.LogTrace(requestLog);
    
    
  2. Next, we want to make sure that whoever is next on the pipeline also has access to the body stream, and as we mentioned in the Overview above, we can’t use “rewind” in the ASP .NET Core stream. So, we simply instantiate a new instance of MemoryStream, write the content of the body to it, and assign it to the request body of the current HTTP context.
    
    public async Task Invoke(HttpContext context)
    {
    	injectedRequestStream = new MemoryStream();
    
        try
    	{
    		var requestLog = 
    		$"REQUEST HttpMethod: {context.Request.Method}, Path: {context.Request.Path}";
    
    		using (var bodyReader = new StreamReader(context.Request.Body))
    		{
    			var bodyAsText = bodyReader.ReadToEnd();
                if (string.IsNullOrWhiteSpace(bodyAsText) == false)
                {
    				requestLog += $", Body : {bodyAsText}";
    			}
    
    			var bytesToWrite = Encoding.UTF8.GetBytes(bodyAsText);
    			injectedRequestStream.Write(bytesToWrite, 0, bytesToWrite.Length);
    			injectedRequestStream.Seek(0, SeekOrigin.Begin);
    			context.Request.Body = injectedRequestStream;
    		}
    
    		_logger.LogTrace(requestLog);
    
    		await _next.Invoke(context);                                
    	}
        finally
        {
    		injectedRequestStream.Dispose();
    	}           
    }   
    
  3. Then we register this custom middleware in the startup class:
    
            public void Configure(
    		IApplicationBuilder app, 
    		IHostingEnvironment env, 
    		ILoggerFactory loggerFactory)
            {
                loggerFactory.AddConsole(Configuration.GetSection("Logging"));
                loggerFactory.AddDebug(LogLevel.Trace);
                
                app.UseMiddleware();
                app.UseMvc();
            }
    

If we now run our application, all http requests with a body should show up in whatever logging mechanism is registered or setup in the application.

I use a client tool – Postman – to hit the sample project API, and the request body is shown on the Visual Studio Debug Output Log screenshot below:

Visual Studio Debug Output Log

Here is a complete solution package (along with a sample Web API controller), that you can download, compile, and run: LogFullRequestResponse-Part-1.

Subscribe

* indicates required
David Hadiprijanto

David is a Senior Software Developer at Palador. When not punching keyboard, in his spare time he enjoys creating sawdust in his home wood shop.