TL;DR - Practical Tips for ASP.NET Core HTTP Logging Middleware
  • Use EnableBuffering() to safely log request bodies and reset stream position after reading.
  • Capture response bodies by swapping HttpResponse.Body with a MemoryStream and restoring after logging.
  • Always filter or redact sensitive data before logging HTTP bodies.
  • Set size limits and skip large or binary payloads to avoid performance issues.
  • Filter logs by endpoint, HTTP method, or custom attributes for clarity.
  • Built-in HttpLogging is simple; custom middleware offers full control and advanced filtering.
  • Use structured logging and external services for production monitoring.
  • Wrap middleware in extension methods for clean, configurable registration.

1. How does EnableBuffering help you log request bodies in ASP.NET Core?

EnableBuffering allows you to read request bodies multiple times without breaking the pipeline.

By default, HttpRequest.Body is a forward-only stream. Once you read it, subsequent middleware gets an empty stream. This breaks model binding and authentication.

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    // Enable buffering so we can read multiple times
    context.Request.EnableBuffering();
    
    // Read the body
    using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
    var body = await reader.ReadToEndAsync();
    
    // Reset position for next middleware
    context.Request.Body.Position = 0;
    
    _logger.LogInformation("Request Body: {Body}", body);
    
    await next(context);
}

EnableBuffering internally replaces the request stream with a FileBufferingReadStream that can seek. For small bodies, it uses memory. For large ones, it spills to disk.

Without this call, you’d get exceptions or empty request bodies downstream. Essential for any middleware that needs to inspect request content while preserving normal request processing.


2. What’s the trick to capturing response bodies using MemoryStream in middleware?

Replace HttpResponse.Body with a MemoryStream, then copy the content back after processing.

Response bodies are write-only streams that go directly to the client. To capture them, you need to intercept the stream temporarily.

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    var originalBodyStream = context.Response.Body;
    
    using var responseBody = new MemoryStream();
    context.Response.Body = responseBody;
    
    await next(context);
    
    // Capture the response
    responseBody.Seek(0, SeekOrigin.Begin);
    var responseText = await new StreamReader(responseBody).ReadToEndAsync();
    
    _logger.LogInformation("Response: {Response}", responseText);
    
    // Copy back to original stream
    responseBody.Seek(0, SeekOrigin.Begin);
    await responseBody.CopyToAsync(originalBodyStream);
    
    context.Response.Body = originalBodyStream;
}

Critical: Always restore the original stream and copy your captured content back. Forgetting this step results in empty responses to clients.


3. Why should you reset the request stream position before passing to the next middleware?

Streams maintain position after reading, failing to reset breaks model binding and authentication.

When you read from HttpRequest.Body, the stream position advances to the end. The next middleware expects to read from the beginning.

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    context.Request.EnableBuffering();
    
    // Read request body
    var body = await context.Request.ReadFromJsonAsync<MyModel>();
    
    // Stream position is now at EOF
    // Next middleware will read empty content!
    
    // Reset position
    context.Request.Body.Position = 0;
    
    await next(context);
}

Common failure scenarios:

  • JWT middleware can’t read bearer tokens from request body
  • Model binding returns null objects
  • Authentication middleware fails silently

Best practice: Always reset to position 0 after reading, even if you think no downstream middleware needs the body. Future code changes might break unexpectedly without this safety net.

Reset immediately after reading, before calling next(context).


How do you avoid logging sensitive data when capturing HTTP bodies?

Use conditional redaction based on content type, headers, or request paths to filter sensitive information.

Never log passwords, tokens, or PII. Implement filtering before writing to logs.

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    var path = context.Request.Path.Value;
    var contentType = context.Request.ContentType;
    
    // Skip sensitive endpoints
    if (path?.Contains("/auth/login") == true || 
        path?.Contains("/payment") == true)
    {
        _logger.LogInformation("Skipped logging for sensitive endpoint: {Path}", path);
        await next(context);
        return;
    }
    
    context.Request.EnableBuffering();
    var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
    context.Request.Body.Position = 0;
    
    // Redact JSON properties
    var sanitized = RedactSensitiveFields(body);
    _logger.LogInformation("Request: {Body}", sanitized);
    
    await next(context);
}

private string RedactSensitiveFields(string json)
{
    return Regex.Replace(json, @"""(password|token|ssn|creditCard)""\s*:\s*""[^""]*""", 
                        @"""$1"":""***""", RegexOptions.IgnoreCase);
}

Production tip: Use allow-lists instead of deny-lists for maximum security.


When should you truncate logs or skip large request/response bodies?

Set size limits to prevent memory issues and log storage bloat, typical threshold is 1KB for debugging, 10KB maximum.

Large payloads can crash applications or fill disk space. Implement size checking before reading streams.

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    const int maxLogSize = 1024; // 1KB
    
    var contentLength = context.Request.ContentLength;
    
    if (contentLength > maxLogSize)
    {
        _logger.LogInformation("Request body too large ({Size} bytes), skipping log", contentLength);
        await next(context);
        return;
    }
    
    context.Request.EnableBuffering();
    var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
    
    if (body.Length > maxLogSize)
    {
        var truncated = body.Substring(0, maxLogSize);
        _logger.LogInformation("Request body (truncated): {Body}...", truncated);
    }
    else
    {
        _logger.LogInformation("Request body: {Body}", body);
    }
    
    context.Request.Body.Position = 0;
    await next(context);
}

File uploads, images, and PDFs should always be skipped. Check Content-Type headers like multipart/form-data or application/octet-stream.


How can you selectively log only specific API endpoints?

Filter by request path, HTTP method, or custom attributes to avoid noise from health checks and static files.

Logging every request creates noise. Target specific endpoints that matter for debugging.

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    // Skip common noise
    var path = context.Request.Path.Value?.ToLower();
    if (path?.StartsWith("/health") == true || 
        path?.StartsWith("/metrics") == true ||
        path?.Contains("/swagger") == true)
    {
        await next(context);
        return;
    }
    
    // Only log API endpoints
    if (!path?.StartsWith("/api/") == true)
    {
        await next(context);
        return;
    }
    
    // Log specific HTTP methods
    if (context.Request.Method is "POST" or "PUT" or "DELETE")
    {
        await LogRequestBody(context);
    }
    
    await next(context);
}

Advanced filtering using endpoint metadata:

var endpoint = context.GetEndpoint();
var shouldLog = endpoint?.Metadata.GetMetadata<LogRequestAttribute>() != null;

if (shouldLog)
{
    await LogRequestBody(context);
}

Create a [LogRequest] attribute for fine-grained control over which controllers or actions get logged.


What are the performance costs of logging HTTP bodies, and how to mitigate them?

HTTP body logging adds 15-50ms latency and significant memory pressure. Use async operations and size limits.

Performance impacts:

  • Memory: Buffering doubles memory usage per request
  • I/O: Additional file/stream operations slow response times
  • CPU: String manipulation and JSON parsing overhead
Mitigation StrategyImpactImplementation
Size limitsHighSkip bodies > 1KB
Async operationsMediumUse ReadToEndAsync()
Conditional loggingHighFilter by endpoint
Background loggingMediumQueue logs, process later
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    // Fast path for large requests
    if (context.Request.ContentLength > 1024)
    {
        await next(context);
        return;
    }
    
    var stopwatch = Stopwatch.StartNew();
    
    // Async operations prevent blocking
    context.Request.EnableBuffering();
    var body = await context.Request.Body.ReadToEndAsync();
    context.Request.Body.Position = 0;
    
    // Background logging
    _ = Task.Run(() => _logger.LogInformation("Request: {Body}", body));
    
    await next(context);
    
    _logger.LogDebug("Logging overhead: {Duration}ms", stopwatch.ElapsedMilliseconds);
}

Production recommendation: Use sampling, log 1% of requests rather than all requests.


How to implement custom middleware vs built-in HttpLogging in ASP.NET Core?

Built-in HttpLogging is simpler but less flexible. Custom middleware gives complete control over filtering and formatting.

FeatureBuilt-in HttpLoggingCustom Middleware
Setup complexityLowMedium
Request body logging✅✅
Response body logging✅✅
Custom filteringLimitedFull control
Performance tuningBasicAdvanced
Sensitive data handlingBasic redactionCustom logic

Built-in HttpLogging:

builder.Services.AddHttpLogging(options =>
{
    options.LoggingFields = HttpLoggingFields.RequestBody | HttpLoggingFields.ResponseBody;
    options.RequestBodyLogLimit = 1024;
});

app.UseHttpLogging();

Custom middleware:

public class CustomHttpLoggingMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // Your custom logic here
        await LogRequest(context);
        await next(context);
        await LogResponse(context);
    }
}

app.UseMiddleware<CustomHttpLoggingMiddleware>();

Choose custom middleware when you need endpoint-specific filtering, custom redaction, or performance optimizations.


Where should you send your logs: console, file, structured, or external service?

Development uses console, staging uses files, production uses structured logging to external services like Seq or ELK.

Logging destinations by environment:

EnvironmentPrimarySecondaryReasoning
DevelopmentConsoleFileImmediate feedback
StagingFileStructuredDebugging integration issues
ProductionExternal serviceFile backupCentralized monitoring
builder.Logging.ClearProviders();

if (builder.Environment.IsDevelopment())
{
    builder.Logging.AddConsole();
}
else if (builder.Environment.IsStaging())
{
    builder.Logging.AddFile("logs/staging-{Date}.txt");
}
else
{
    builder.Logging.AddSeq("https://seq.company.com");
    builder.Logging.AddFile("logs/production-{Date}.txt"); // Backup
}

Structured logging for production:

_logger.LogInformation("HTTP Request processed {@RequestData}", new {
    Method = context.Request.Method,
    Path = context.Request.Path,
    StatusCode = context.Response.StatusCode,
    Duration = stopwatch.ElapsedMilliseconds
});

External services enable dashboards, alerts, and correlation across multiple application instances.


How to wrap this middleware implementation behind a clean extension method?

Create an extension method on IApplicationBuilder for clean registration and configuration options.

public static class HttpLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomHttpLogging(
        this IApplicationBuilder app, 
        Action<HttpLoggingOptions>? configure = null)
    {
        var options = new HttpLoggingOptions();
        configure?.Invoke(options);
        
        return app.UseMiddleware<CustomHttpLoggingMiddleware>(options);
    }
}

public class HttpLoggingOptions
{
    public int MaxBodySize { get; set; } = 1024;
    public string[] SkipPaths { get; set; } = { "/health", "/metrics" };
    public bool LogResponseBodies { get; set; } = true;
    public string[] SensitiveFields { get; set; } = { "password", "token" };
}

Clean usage in Program.cs:

app.UseCustomHttpLogging(options =>
{
    options.MaxBodySize = 2048;
    options.SkipPaths = new[] { "/health", "/swagger" };
    options.LogResponseBodies = false;
});

Middleware constructor:

public CustomHttpLoggingMiddleware(RequestDelegate next, HttpLoggingOptions options)
{
    _next = next;
    _options = options;
}

This pattern provides a clean API while keeping configuration flexible and testable.


To summarize, these tips help you implement robust HTTP logging middleware in ASP.NET Core, ensuring you can capture and analyze request and response bodies effectively while maintaining performance and security. By following these practices, you can create a logging solution that is both powerful and efficient, tailored to your application’s needs.

See other aspnet-core posts