Logging Requests and Responses for APIs and HttpClient in ASP.NET Core
Learn to log incoming API requests and outgoing HttpClient calls in ASP.NET Core using built-in HTTP logging and a custom DelegatingHandler, step by step.
A receipt for every visit to the shop
Think about a busy sweet shop in your town. People walk in all day. Some buy laddoos, some only ask the price, and some come to return a box that was wrong. At the end of the day, the owner wants to know what happened. Who came in? What did they ask for? Did they leave happy or angry?
A smart shop owner keeps two small notebooks. One notebook is for people who walk into the shop — what they wanted and what they got. The second notebook is for trips the owner makes to other shops — like going to the dairy to buy fresh milk. "I asked for 5 litres, they gave me 5 litres, the bill was 250 rupees."
Your web API is exactly like this shop. Other programs walk in and make requests. Your API also walks out to other APIs using something called HttpClient. To run a healthy service, you want both notebooks: one for requests that come in, and one for requests that go out.
In this guide we will fill both notebooks using ASP.NET Core on .NET 10. The good news is that most of the work is already built for you.
Why log at all?
When something breaks at 2 in the morning, you cannot ask the user "what exactly did you click?" The logs are your only witness. Good request and response logs help you:
- See which endpoints are slow or failing.
- Understand what data a caller actually sent.
- Prove what a third-party API returned when they say "our side is fine".
- Trace one user journey across many services using a shared ID.
Let us start with the first notebook: requests that come into your API.
Part 1 — Logging incoming requests with built-in HTTP logging
Since .NET 6, ASP.NET Core ships a middleware called HTTP logging. It lives in the Microsoft.AspNetCore.HttpLogging package, which is already part of the framework. You do not install anything extra. You just switch it on.
There are two steps. First you register the service. Second you add the middleware to the pipeline.
var builder = WebApplication.CreateBuilder(args);
// 1. Register HTTP logging and choose what to log
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields =
HttpLoggingFields.RequestPropertiesAndHeaders |
HttpLoggingFields.ResponsePropertiesAndHeaders |
HttpLoggingFields.Duration;
// Optional: add a header to the log that is normally hidden
options.RequestHeaders.Add("X-Correlation-Id");
});
var app = builder.Build();
// 2. Turn the middleware on, early in the pipeline
app.UseHttpLogging();
app.MapGet("/hello", () => "Hello from the API");
app.Run();By default the middleware logs common properties: the path, the method, the status code, and the headers. The LoggingFields setting is an enum flag, so you can mix and match the parts you care about with the | symbol.
There is one more step that trips up almost everyone. The logs are written at the Information level under a special category. If your logging level is set higher than that, you will see nothing. Open appsettings.json and add this line.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
}Now run the app and call GET /hello. You will see a tidy log entry with the path, the headers, the status code, and how long the request took.
Built-in HTTP logging setup
Steps
AddHttpLogging
Register and pick fields
UseHttpLogging
Add middleware early
Set log level
Information in appsettings
Choosing what to log
The HttpLoggingFields enum lets you control exactly what ends up in the notebook. Here are the common choices.
| Field | What it logs | Safe by default? |
|---|---|---|
RequestPropertiesAndHeaders | Method, path, protocol, request headers | Yes |
ResponsePropertiesAndHeaders | Status code, response headers | Yes |
RequestBody | The raw request body | No, can hold secrets |
ResponseBody | The raw response body | No, can be huge |
Duration | How long the request took | Yes |
All | Everything above | Use with care |
A good starting point for most APIs is properties and headers plus duration. Add bodies only when you are chasing a specific bug.
A note on logging bodies
Bodies are tempting but dangerous. A login request body holds a password. A payment body holds card details. A file upload body could be many megabytes. Reading and buffering all of that costs memory and can slow your service under load.
If you must log a body, set a hard size limit so a giant upload does not flood your logs.
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields = HttpLoggingFields.All;
// Never buffer more than these many bytes
options.RequestBodyLogLimit = 4096;
options.ResponseBodyLogLimit = 4096;
// Hide values for sensitive headers
options.RequestHeaders.Remove("Authorization");
});Even with limits, redact secrets first. The kindest rule is simple: if you would not write it on a postcard, do not put it in a log.
Part 2 — Logging outgoing HttpClient calls
Now for the second notebook. Your API often calls other APIs. Maybe a weather service, a payment gateway, or another microservice you own. These calls go through HttpClient, and the best way to make and log them is with IHttpClientFactory.
A little free logging
When you create clients through IHttpClientFactory, you already get some logging for free. The factory adds two helpers to every client. One records a log scope around the whole request, and the other writes a short log line about the request and the response. You can see these by setting this category to Information:
{
"Logging": {
"LogLevel": {
"System.Net.Http.HttpClient": "Information"
}
}
}This gives you the method, the URL, the status code, and the time taken. For many apps that is enough. But when you need richer detail — like specific headers or a trimmed body — you write your own handler.
Writing a DelegatingHandler
A DelegatingHandler is a small class that sits in the middle of every outgoing call. Think of it as a checkpoint on the road. The request stops at the checkpoint, you write a note, you let it pass, the response comes back, and you write another note.
You inherit from DelegatingHandler and override one method called SendAsync. You log before calling base.SendAsync, and you log after.
public sealed class LoggingHandler : DelegatingHandler
{
private readonly ILogger<LoggingHandler> _logger;
public LoggingHandler(ILogger<LoggingHandler> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var watch = Stopwatch.StartNew();
_logger.LogInformation(
"Outgoing {Method} call to {Url}",
request.Method,
request.RequestUri);
var response = await base.SendAsync(request, cancellationToken);
watch.Stop();
_logger.LogInformation(
"Got {StatusCode} from {Url} in {Elapsed} ms",
(int)response.StatusCode,
request.RequestUri,
watch.ElapsedMilliseconds);
return response;
}
}Notice we use message templates with named holes like {Method} and {Url}. This keeps each value as a real searchable field, not just a glued string. That habit pays off when you later search your logs.
Registering the handler
Handlers must be registered as transient services. Then you attach the handler to a named or typed client with AddHttpMessageHandler.
builder.Services.AddTransient<LoggingHandler>();
builder.Services
.AddHttpClient("weather", client =>
{
client.BaseAddress = new Uri("https://api.weather.example/");
})
.AddHttpMessageHandler<LoggingHandler>();Now every call made through the weather client passes through your checkpoint and gets logged. You did not have to touch the calling code at all.
DelegatingHandler logging flow
Steps
Log request
Method and URL
base.SendAsync
Call the real API
Log response
Status and time
Return
Pass response back
Where your handler sits in the pipeline
The factory builds a chain of handlers, like a row of toll gates. Your LoggingHandler is one gate. If you also add resilience handlers (for retries), the order matters. A handler added first sees the request first.
One thing to watch: if you put logging before a retry handler, you log once per overall call. If you put it after the retry handler, you log once per attempt. Both can be useful. Choose based on whether you want to see every retry or just the final result.
Part 3 — Tying both notebooks together with a correlation ID
Here is where the magic happens. Imagine a request comes into your API, and to answer it your API calls two other services. If something breaks, you want to follow that single journey across all three logs. The trick is a correlation ID: one unique string that travels with the request everywhere.
You read or create it when the request arrives, store it, and then attach it to every outgoing call inside your LoggingHandler.
app.Use(async (context, next) =>
{
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString();
// Make it available to logs for this request
using (context.RequestServices
.GetRequiredService<ILogger<Program>>()
.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId }))
{
context.Response.Headers["X-Correlation-Id"] = correlationId;
await next();
}
});Now both the incoming log and the outgoing log carry the same CorrelationId. When a customer reports a problem and gives you that ID, you can pull up the entire story in seconds.
Built-in vs custom: which to use?
You do not always have to write code. Often the built-in tools are enough. Here is a simple way to decide.
| Need | Best tool | Write code? |
|---|---|---|
| Log incoming requests quickly | Built-in HTTP logging middleware | No |
| Log incoming bodies with limits | HTTP logging with body fields | Small config |
| Basic outgoing call logs | IHttpClientFactory default logging | No |
| Rich outgoing logs, custom fields | Custom DelegatingHandler | Yes |
| Trace across services | Correlation ID + scopes | A little |
Start with the built-in options. Add custom handlers only when the built-in logs cannot answer your questions. Less code means fewer bugs.
A few friendly warnings
Logging is helpful, but a careless setup can hurt you. Keep these in mind.
- Secrets. Strip
Authorization,Cookie, passwords, and tokens before they reach a log. The middleware lets you remove sensitive headers from the allowed list. - Size. Bodies can be large. Always set
RequestBodyLogLimitandResponseBodyLogLimitwhen you log bodies. - Volume. A busy API can write millions of lines a day. Send logs to a real sink like Seq, Elasticsearch, or a cloud log service, and set sensible levels.
- Personal data. Names, emails, and addresses may be protected by law. Log only what you truly need.
- Performance. Reading bodies forces buffering. Measure before turning it on everywhere.
If you also use a structured logging library like Serilog, these logs flow straight into it, and you can search them by field. That pairs beautifully with everything above.
A safe logging checklist
Steps
Redact secrets
No passwords in logs
Limit body size
Cap the bytes
Pick a fast sink
Seq, cloud, etc
Add correlation ID
Trace journeys
Putting it all in Program.cs
Here is a compact version that brings the incoming and outgoing pieces together, so you can see the whole shape in one place.
var builder = WebApplication.CreateBuilder(args);
// Incoming request logging
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields =
HttpLoggingFields.RequestPropertiesAndHeaders |
HttpLoggingFields.ResponsePropertiesAndHeaders |
HttpLoggingFields.Duration;
});
// Outgoing call logging
builder.Services.AddTransient<LoggingHandler>();
builder.Services
.AddHttpClient("weather", c => c.BaseAddress = new Uri("https://api.weather.example/"))
.AddHttpMessageHandler<LoggingHandler>();
var app = builder.Build();
app.UseHttpLogging();
app.MapGet("/forecast", async (IHttpClientFactory factory) =>
{
var client = factory.CreateClient("weather");
var text = await client.GetStringAsync("today");
return Results.Ok(text);
});
app.Run();That is a complete, working setup. Incoming requests are logged by the middleware, and the outgoing call to the weather API is logged by your handler.
Quick recap
- Your API has two kinds of traffic: requests that come in, and calls that go out. Log both.
- For incoming requests, use the built-in HTTP logging middleware:
AddHttpLogging, thenUseHttpLogging, then set the log level toInformationinappsettings.json. - Use
HttpLoggingFieldsto pick exactly what to log. Headers and status are safe; bodies are risky. - For outgoing
HttpClientcalls,IHttpClientFactorygives basic logs for free. For more detail, write aDelegatingHandlerthat overridesSendAsync, register it as transient, and attach it withAddHttpMessageHandler. - Use message templates with named holes so each value stays searchable.
- Tie everything together with a correlation ID so you can follow one journey across many services.
- Always redact secrets, limit body sizes, and send logs to a fast sink.
References and further reading
- HTTP logging in .NET and ASP.NET Core — Microsoft Learn
- Make HTTP requests using IHttpClientFactory — Microsoft Learn
- Extending HttpClient With Delegating Handlers in ASP.NET Core — Milan Jovanovic
- Exploring the code behind IHttpClientFactory — Andrew Lock
- Request-Response Logging in ASP.NET Core — Egor Tarasov (Medium)
Related Posts
5 Serilog Best Practices for Better Structured Logging in .NET
Learn 5 simple Serilog best practices for structured logging in .NET: message templates, enrichers, correlation IDs, hiding secrets, and async sinks.
Getting Started With OpenTelemetry in .NET With Jaeger and Seq
A beginner guide to OpenTelemetry in .NET. Add traces, metrics, and logs, then view them in Jaeger and Seq using the OTLP exporter step by step.
Retries and Resilience in .NET with Polly and Microsoft Resilience
Learn retries, timeouts, and circuit breakers in .NET using Polly v8 and Microsoft.Extensions.Http.Resilience, with simple examples a beginner can follow.
Extending HttpClient With Delegating Handlers in ASP.NET Core
Learn how DelegatingHandlers build a middleware pipeline for HttpClient in ASP.NET Core to add logging, auth, and retries with IHttpClientFactory.
Logging Best Practices in ASP.NET Core: A Beginner's Guide
Learn logging best practices in ASP.NET Core: log levels, structured logging, source-generated LoggerMessage, scopes, correlation IDs, and keeping secrets out.
Structured Logging in ASP.NET Core with Serilog: A Beginner's Guide
A friendly, step-by-step guide to structured logging in ASP.NET Core with Serilog: setup, sinks, request logging, and viewing logs on .NET 10.