The right way of getting http request body from metric handler
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 281
Description
ASP .NET Core 8 app.
I listen to the `http.server.request.duration` metric:
```
_meterListener = new MeterListener
{
InstrumentPublished = (instrument, listener) =>
{
if (instrument.Name is "http.server.request.duration")
{
listener.EnableMeasurementEvents(instrument);
}
}
};
_meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
_meterListener.Start();
```
Inside the measurement callback I want to get the request body:
```
private void OnMeasurementRecorded(Instrument instrument, double measurement, ReadOnlySpan> tags, object? state)
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext is null)
return;
var requestBody = GetRequestBodySync(httpContext);
// some other stuff
}
private static string GetRequestBodySync(HttpContext context)
{
string body = string.Empty;
if (context.Request.Method == HttpMethods.Post && context.Request.ContentLength > 0)
{
var currentPosition = context.Request.Body.Position;
try
{
context.Request.Body.Position = 0;
StringBuilder builder = new StringBuilder();
byte[] buffer = ArrayPool.Shared.Rent(4096);
while (true)
{
var bytesRemaining = context.Request.Body.Read(buffer, offset: 0, buffer.Length);
if (bytesRemaining == 0)
break;
var encodedString = Encoding.UTF8.GetString(buffer, 0, bytesRemaining);
builder.Append(encodedString);
}
ArrayPool.Shared.Return(buffer);
body = builder.ToString();
context.Items[RequestBodyItemKey] = body;
}
finally
{
context.Request.Body.Position = currentPosition;
}
}
return body;
}
```
Everything works fine both on my local machine and on test environment. But when I deploy this to the production, where the number of requests is large, the app is down. Along with this I have a lot of the following exceptions: `System.ObjectDisposedException, Cannot access a closed Stream`. The exceptions point to the following line: `var currentPosition = context.Request.Body.Position;`
I made some tests on my local machine, but I couldn't reproduce this behavior.
I found a place where the metric writes: https://github.com/dotnet/aspnetcore/blob/25ef4aa38b77974263cb10f6e9cbd10135f17b59/src/Hosting/Hosting/src/Internal/HostingApplication.cs#L93
```
public void DisposeContext(Context context, Exception? exception)
{
var httpContext = context.HttpContext!;
_diagnostics.RequestEnd(httpContext, exception, context);
if (_defaultHttpContextFactory != null)
{
_defaultHttpContextFactory.Dispose((DefaultHttpContext)httpContext);
if (_defaultHttpContextFactory.HttpContextAccessor != null)
{
// Clear the HttpContext if the accessor was used. It's likely that the lifetime extends
// past the end of the http request and we want to avoid changing the reference from under
// consumers.
context.HttpContext = null;
}
}
else
{
_httpContextFactory!.Dispose(httpContext);
}
_diagnostics.ContextDisposed(context);
// Reset the context as it may be pooled
context.Reset();
}
```
Looking at the code above we can see that the metric writes before the `Dispose()` is called. The `RequestEnd()` method is synchronous, so it looks like everything should work fine. But it only looks... I put `Thread.Sleep()` inside the `OnMeasurementRecorded()` method and figured out that http response comes much faster than thread is actually sleeping.
My questions are:
1. At what point http request body is actually disposed?
2. Will it work if I save http request to the `HttpContext.Items` collection from my middleware to take it further inside the `OnMeasurementRecorded` handler?
Contributor guide
Assessment
This issue has not been assessed yet.