TL;DR
- Use custom middleware to log HTTP request and response bodies in ASP.NET Core for better debugging and diagnostics.
- Implement stream rewinding to read request bodies without breaking downstream middleware.
- Always redact sensitive data and use selective logging to avoid performance and security issues.
- Handle large bodies by truncating logs and excluding static or health check endpoints.
- Consider built-in
HttpLogging
for simple scenarios, but use custom middleware for full control and compliance.
We’ve all been there, stuck debugging an API issue for hours, wishing we could just see what’s actually coming in and going out of our application. That’s exactly what request and response body logging solves.
I’m going to walk you through how to build a practical logging solution for HTTP traffic in ASP.NET Core using custom middleware.
Why Log HTTP Request and Response Bodies?
Have you ever spent hours banging your head against the wall trying to debug an API issue? I certainly have. There’s that moment of pure frustration when you know something’s wrong with the data flowing through your system, but you can’t quite put your finger on it. That’s exactly when request and response body logging becomes your best friend.
I remember one particularly painful debugging session where a client swore up and down they were sending the correct JSON payload. After adding proper request logging, we discovered they were actually sending an array where we expected an object. Five minutes to implement logging saved us days of back-and-forth troubleshooting.
When you’re integrating with third-party services, this visibility becomes even more valuable. Ever tried to figure out why that payment processor keeps rejecting your perfectly valid requests? With request/response logging, you can see precisely what’s going back and forth, often revealing subtle formatting issues or missing headers that documentation never mentioned.
For many of my clients in healthcare and finance, logging isn’t just helpful, it’s a compliance requirement. Being able to prove exactly who did what and when provides the audit trail that keeps auditors happy and systems secure. These logs have saved my clients more than once when security questions arose about specific transactions.
What I love most about implementing custom logging is how it helps spot patterns over time.
Users might report intermittent issues that are nearly impossible to reproduce, but when you review the logs, you might notice that specific input structures or unusual edge cases consistently trigger problems. These insights are gold for improving system stability.
While ASP.NET Core does include built-in HttpLogging middleware that handles some basic scenarios, building your own solution gives you complete control.
You can decide exactly what gets captured, how sensitive data is handled, and where those logs end up. This level of customization is critical when you’re dealing with real-world applications where one size definitely doesn’t fit all.
To understand request throttling, you can refer to my post on Request Throttling Middleware with MemoryCache and IP Limits.
The Challenge with Logging Bodies in ASP.NET Core
Logging bodies in ASP.NET Core comes with a couple of tricky issues:
Request bodies can only be read once, Once you read the request body stream, it’s done, other middleware down the pipeline won’t be able to read it again.
Response bodies don’t exist yet when your middleware runs, You need a special approach to capture what gets written to the response after your code runs.
Let’s solve both problems with a practical example that you can use in your projects.

HTTP Request and Response Flow Through Logging Middleware
Creating the Request/Response Logging Middleware
Let’s start by creating a middleware that can log both request and response bodies:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
public RequestResponseLoggingMiddleware(RequestDelegate next, ILogger<RequestResponseLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// First, store the original body stream
var originalRequestBody = context.Request.Body;
var originalResponseBody = context.Response.Body;
try
{
// Read and log the request body
string requestBody = await ReadRequestBodyAsync(context);
LogRequest(context, requestBody);
// Create a new memory stream for the response
using var responseBodyStream = new MemoryStream();
context.Response.Body = responseBodyStream;
// Continue the pipeline
await _next(context);
// Read and log the response body
string responseBody = await ReadResponseBodyAsync(context.Response);
LogResponse(context, responseBody);
// Copy the response to the original stream and restore it
await responseBodyStream.CopyToAsync(originalResponseBody);
}
finally
{
// Always restore the original streams
context.Request.Body = originalRequestBody;
context.Response.Body = originalResponseBody;
}
}
private async Task<string> ReadRequestBodyAsync(HttpContext context)
{
// We need to read the request body and then rewind it
context.Request.EnableBuffering();
using var streamReader = new StreamReader(
context.Request.Body,
encoding: Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
leaveOpen: true);
var requestBody = await streamReader.ReadToEndAsync();
// IMPORTANT: Reset the position to 0 to allow other middleware to read it
context.Request.Body.Position = 0;
return requestBody;
}
private async Task<string> ReadResponseBodyAsync(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
var responseBody = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return responseBody;
}
private void LogRequest(HttpContext context, string requestBody)
{
_logger.LogInformation(
"HTTP {Method} {Path} received at {Time}\n" +
"Request Headers: {Headers}\n" +
"Request Body: {Body}",
context.Request.Method,
context.Request.Path,
DateTime.UtcNow,
FormatHeaders(context.Request.Headers),
requestBody);
}
private void LogResponse(HttpContext context, string responseBody)
{
_logger.LogInformation(
"HTTP {StatusCode} returned for {Method} {Path}\n" +
"Response Headers: {Headers}\n" +
"Response Body: {Body}",
context.Response.StatusCode,
context.Request.Method,
context.Request.Path,
FormatHeaders(context.Response.Headers),
responseBody);
}
private string FormatHeaders(IHeaderDictionary headers)
{
var sb = new StringBuilder();
foreach (var (key, value) in headers)
{
sb.AppendLine($"{key}: {string.Join(", ", value)}");
}
return sb.ToString();
}
}
Let’s understand what’s happening here:
- We’re using
EnableBuffering()
to make the request body readable multiple times - We’re replacing the response body with a memory stream that we control
- After the next middleware completes, we read what was written to our memory stream
- Finally, we copy our captured response back to the original stream
Creating an Extension Method for Clean Registration
Let’s add an extension method to make it easy to add our middleware to the pipeline:
using Microsoft.AspNetCore.Builder;
public static class RequestResponseLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestResponseLoggingMiddleware>();
}
}
Adding the Middleware to the Pipeline
In your Program.cs
or Startup.cs
, add the middleware to your pipeline:
// Program.cs in ASP.NET Core 6+
var builder = WebApplication.CreateBuilder(args);
// Add services...
var app = builder.Build();
// Add the logging middleware early in the pipeline
app.UseRequestResponseLogging();
// Other middleware...
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Making the Logging Middleware Smarter
The basic implementation works, but let’s enhance it to handle real-world scenarios better:
1. Adding Selective Logging
You probably don’t want to log every request and response, especially for static files or health checks:
public class RequestResponseLoggingOptions
{
public bool LogRequests { get; set; } = true;
public bool LogResponses { get; set; } = true;
public List<string> ExcludedPaths { get; set; } = new List<string>();
public int? MaxBodyLogSize { get; set; } = 4096; // Limit logging to 4KB by default
// Add a method to check if a path should be logged
public bool ShouldLog(PathString path)
{
return !ExcludedPaths.Any(p => path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase));
}
}
// Updated middleware constructor
public RequestResponseLoggingMiddleware(
RequestDelegate next,
ILogger<RequestResponseLoggingMiddleware> logger,
RequestResponseLoggingOptions options)
{
_next = next;
_logger = logger;
_options = options;
}
// Use in InvokeAsync
public async Task InvokeAsync(HttpContext context)
{
// Skip logging if path is excluded
if (!_options.ShouldLog(context.Request.Path))
{
await _next(context);
return;
}
// Rest of the implementation...
}
2. Handling Different Content Types
Not all requests and responses are JSON. Let’s make our middleware smarter about what content it should log:
private bool CanLogBody(string contentType)
{
if (string.IsNullOrEmpty(contentType))
return false;
return contentType.Contains("json") ||
contentType.Contains("xml") ||
contentType.Contains("text/") ||
contentType.Contains("form-urlencoded");
}
public async Task InvokeAsync(HttpContext context)
{
var originalRequestBody = context.Request.Body;
var originalResponseBody = context.Response.Body;
try
{
// Only log request body if appropriate content type
string requestBody = "";
if (_options.LogRequests && CanLogBody(context.Request.ContentType))
{
requestBody = await ReadRequestBodyAsync(context);
LogRequest(context, requestBody);
}
else if (_options.LogRequests)
{
LogRequestMetadataOnly(context);
}
// Handle response...
}
finally
{
// Always restore streams...
}
}
3. Handling Large Bodies
For large request or response bodies, you might want to truncate them:
private string TruncateIfNeeded(string content)
{
if (_options.MaxBodyLogSize.HasValue && content.Length > _options.MaxBodyLogSize.Value)
{
return content.Substring(0, _options.MaxBodyLogSize.Value) +
$"... [Truncated after {_options.MaxBodyLogSize.Value} characters]";
}
return content;
}
A Complete Real-World Implementation
Here’s a more complete implementation that brings together all these concepts:
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
private readonly RequestResponseLoggingOptions _options;
public RequestResponseLoggingMiddleware(
RequestDelegate next,
ILogger<RequestResponseLoggingMiddleware> logger,
RequestResponseLoggingOptions options = null)
{
_next = next;
_logger = logger;
_options = options ?? new RequestResponseLoggingOptions();
}
public async Task InvokeAsync(HttpContext context)
{
// Skip excluded paths
if (!_options.ShouldLog(context.Request.Path))
{
await _next(context);
return;
}
var originalRequestBody = context.Request.Body;
var originalResponseBody = context.Response.Body;
try
{
// Handle request
if (_options.LogRequests)
{
if (CanLogBody(context.Request.ContentType))
{
string requestBody = await ReadRequestBodyAsync(context);
LogRequest(context, TruncateIfNeeded(requestBody));
}
else
{
LogRequestMetadataOnly(context);
}
}
// Handle response
if (_options.LogResponses)
{
using var responseBodyStream = new MemoryStream();
context.Response.Body = responseBodyStream;
await _next(context);
if (CanLogBody(context.Response.ContentType))
{
string responseBody = await ReadResponseBodyAsync(context.Response);
LogResponse(context, TruncateIfNeeded(responseBody));
}
else
{
LogResponseMetadataOnly(context);
}
await responseBodyStream.CopyToAsync(originalResponseBody);
}
else
{
await _next(context);
}
}
finally
{
context.Request.Body = originalRequestBody;
context.Response.Body = originalResponseBody;
}
}
// Helper methods as shown earlier...
private void LogRequestMetadataOnly(HttpContext context)
{
_logger.LogInformation(
"HTTP {Method} {Path} received at {Time} [Body logging skipped - unsupported content type]",
context.Request.Method,
context.Request.Path,
DateTime.UtcNow);
}
private void LogResponseMetadataOnly(HttpContext context)
{
_logger.LogInformation(
"HTTP {StatusCode} returned for {Method} {Path} [Body logging skipped - unsupported content type]",
context.Response.StatusCode,
context.Request.Method,
context.Request.Path);
}
}
Creating a More Flexible Registration
Let’s update our extension method to accept options:
public static class RequestResponseLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestResponseLogging(
this IApplicationBuilder builder,
Action<RequestResponseLoggingOptions> configureOptions = null)
{
var options = new RequestResponseLoggingOptions();
configureOptions?.Invoke(options);
return builder.UseMiddleware<RequestResponseLoggingMiddleware>(options);
}
}
This allows us to use it like this:
app.UseRequestResponseLogging(options => {
options.ExcludedPaths.Add("/healthcheck");
options.ExcludedPaths.Add("/swagger");
options.MaxBodyLogSize = 8192; // 8KB
});
Example: Real-World Application
Let’s see how this middleware might be used in a real API controller:
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public async Task<ActionResult<OrderResponse>> CreateOrder(OrderRequest request)
{
// With our logging middleware, we'll see:
// 1. The full incoming OrderRequest JSON in the logs
// 2. The full outgoing OrderResponse JSON in the logs
var order = await _orderService.CreateOrderAsync(request);
return Ok(new OrderResponse { OrderId = order.Id, Status = order.Status });
}
}
Handling Sensitive Data
Let’s face it, you don’t want passwords and credit cards showing up in your logs. Here’s a simple way to redact sensitive info:
private string RedactSensitiveInfo(string content, string contentType)
{
// Only try to redact if it looks like JSON
if (contentType?.Contains("json") == true && !string.IsNullOrEmpty(content))
{
try
{
// Simple regex replacements for common sensitive fields
content = Regex.Replace(content, "\"password\":\"[^\"]*\"", "\"password\":\"[REDACTED]\"", RegexOptions.IgnoreCase);
content = Regex.Replace(content, "\"creditCard\":\"[^\"]*\"", "\"creditCard\":\"[REDACTED]\"", RegexOptions.IgnoreCase);
content = Regex.Replace(content, "\"ssn\":\"[^\"]*\"", "\"ssn\":\"[REDACTED]\"", RegexOptions.IgnoreCase);
// For a more robust solution, you'd want to use a JSON parser
// and systematically identify and redact sensitive fields
}
catch
{
// If redaction fails, fall back to safe behavior
return "[Content contained sensitive data - redaction failed]";
}
}
return content;
}
Performance Considerations
Logging request and response bodies can impact performance. Here are some things to keep in mind:
Consideration | Impact | Mitigation |
---|---|---|
Memory usage | Each request/response body is temporarily stored in memory | Use MaxBodyLogSize to limit size |
I/O operations | Extra disk writes for logging | Use asynchronous logging |
CPU overhead | Parsing and redacting data | Implement selective logging by path |
Response time | Slight increase due to stream manipulation | Profile and optimize for your use case |
Using the Built-in HttpLogging (Alternative Approach)
ASP.NET Core also includes built-in HTTP logging middleware that can handle some of these scenarios. If your needs are simpler, you might consider using it:
// In Program.cs
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestHeaders.Add("Authorization");
logging.RequestHeaders.Add("X-Real-IP");
logging.ResponseHeaders.Add("X-Response-Time");
logging.MediaTypeOptions.AddText("application/json");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;
});
// In middleware pipeline
app.UseHttpLogging();
The built-in solution is more limited but might be sufficient for simpler scenarios.
Where to Log: Console vs File vs Service
Once you have request and response bodies, you need to decide where to log them. Here are some options:
- Console logging - Good for development, easy to set up
- File logging - Good for simple production systems
- Structured logging - Using something like Serilog or NLog
- Centralized logging - Send logs to Elasticsearch, Splunk, or similar
Here’s how to set up structured logging with Serilog:
// In Program.cs
builder.Host.UseSerilog((ctx, services, lc) => lc
.ReadFrom.Configuration(ctx.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File("logs/requests-.txt", rollingInterval: RollingInterval.Day));
Conclusion
Adding request and response body logging to your ASP.NET Core app gives you x-ray vision when debugging API problems. You can see exactly what’s going in and out, something that’s worth its weight in gold when you’re stuck on a tough issue.
Just remember these key takeaways:
- Use
EnableBuffering()
so request bodies can be read multiple times - Swap the response body with a memory stream to capture what gets written
- Always redact sensitive data before it hits your logs
- Be smart about what you log to avoid performance issues
- Consider the built-in
HttpLogging
for simpler cases
Logging responses is especially powerful when combined with a clean response model, check out my guide on Clean ASP.NET Core API Response Patterns to design consistent envelopes for your JSON outputs
Implement this in your projects, and you’ll thank yourself next time you’re debugging a tricky API integration. Good luck and happy logging!
Frequently Asked Questions
How can I log request and response bodies in ASP.NET Core?
Why would I need to log HTTP request and response bodies?
Is there a performance impact when logging request and response bodies?
Does ASP.NET Core have built-in request and response body logging?
How can I avoid logging sensitive data from request and response bodies?
Can I log request bodies without affecting downstream middleware?
How do I handle large request or response bodies in my logging middleware?
How can I log multipart form data and file uploads?
Where should logging middleware be placed in the middleware pipeline?
See other aspnet-core posts
- How to Prevent Common Web Attacks in ASP.NET Core: Security Best Practices and Example
- Stop Repeating Yourself: Cleaner API Responses in ASP.NET Core
- Custom routing constraint in AspNet core
- Understanding dotnet dev-certs https: Local HTTPS for .NET Development
- ASP.NET Core HTTP Logging Middleware: 10 Practical Micro Tips
- Recommended Middleware Order in ASP.NET Core for Secure, Fast, and Correct Pipelines
- ASP.NET Core Middleware: Difference Between Use, Run, and Map Explained
- Implementing Request Throttling Middleware in ASP.NET Core Using MemoryCache and Per-IP Limits