Skip to content

The right way of getting http request body from metric handler #61347

Open
@verdysh

Description

@verdysh

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<double>(OnMeasurementRecorded);
_meterListener.Start();

Inside the measurement callback I want to get the request body:

private void OnMeasurementRecorded(Instrument instrument, double measurement, ReadOnlySpan<KeyValuePair<string, object?>> 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<byte>.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<byte>.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:

public void DisposeContext(Context context, Exception? exception)

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions