dotnet / dotnet/aspnetcore

The right way of getting http request body from metric handler

Open
#61,347 0 comments 0 reactions 0 assignees View on GitHub
area-networking
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.