10

I’m trying to log out Web API Request Content – i.e. the json string. I implemented an ITraceWriter class (example) and configured it so that Web API calls it in the pipeline. But if I read the request.Content or copy into to a stream to read it is not available for the method resulting in a null model. This post talks about that issue a little. Anyone have experience logging out inbound Web API request content and know what the best approach is?

Thanks

Update A

I created a simple sample Web API project to rule out anything in my project and I still see that the model will be null because of logging. I simply test a few times in a row by posting via Fidder and see my model comes in null. With breakpoints in place, it might work which is why I think there is a sync/timing issue. Any thoughts on how to get this to work?

Header:

User-Agent: Fiddler
Host: localhost:56824
Content-Type: application/json
Content-Length: 22

Body:

{
"A":1,"B":"test"
}

Here's the code:

Controller:

public class ValuesController : ApiController
{
    [HttpPost]
    public void Post(ValuesModel model)
    {
        if (model == null)
        {
            Debug.WriteLine("model was null!");
        }
        else
        {
            Debug.WriteLine("model was NOT null!");
        }
    }
}

Model:

public class ValuesModel
{
    public int A { get; set; }
    public string B { get; set; }
}

Logger:

public class APITraceLogger : DelegatingHandler
    {
        protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            if (request.Content != null)
            {
                // This can cause model to be null
                request.Content.ReadAsStringAsync().ContinueWith(s =>
                {
                    string requestText = s.Result;
                    Debug.WriteLine(requestText);
                });

                // and so can this
                //request.Content.ReadAsByteArrayAsync()
                //    .ContinueWith((task) =>
                //    {
                //        string requestText = System.Text.UTF8Encoding.UTF8.GetString(task.Result);
                //        Debug.WriteLine(requestText);
                //    });
            }
            // Execute the request, this does not block
            var response = base.SendAsync(request, cancellationToken);

            // TODO:
            // Once the response is processed asynchronously, log the response data
            // to the database


            return response;
        }


    }

Wiring up logger in WebApiConfig class:

config.MessageHandlers.Add(new APITraceLogger());

Update B

It seems like it is now working if I change the logger to the following code adding the await, async and returning the result. Seems like something I'm not understanding in the async code or truly a timing issue or something.

public class APITraceLogger : DelegatingHandler
{
    protected async override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        if (request.Content != null)
        {

            // This does seem to work - is it because it is synchronous?  Is this a potential problem?
            var requestText = await request.Content.ReadAsStringAsync();
            Debug.WriteLine(requestText);
        }
        // Execute the request, this does not block
        var response = base.SendAsync(request, cancellationToken);

        // TODO:
        // Once the response is processed asynchronously, log the response data
        // to the database


        return response.Result;
    }


}
Community
  • 1
  • 1
Bryan
  • 857
  • 1
  • 11
  • 25

1 Answers1

6

As Filip mentions in that post ReadAsStringAsync or ReadAsByteArrayAsync methods buffer the request content internally. This means that even if your incoming request's stream type is a non-buffered stream, you could safely do a ReadAsStringAsync/ReadAsByteArrayAsync at a message handler for example, and also expect the model binding to work fine.

By default, a request's stream is buffered in both webhost and selfhost cases. But if you would like to check if using ReadAsStringAsync/ReadAsByteArrayAsync and model biding works fine even in non-buffered mode, you can do the following to force non-buffered mode:

public class CustomBufferPolicySelector : WebHostBufferPolicySelector
{
    public override bool UseBufferedInputStream(object hostContext)
    {
        //NOTE: by default, the request stream is always in buffered mode.
        //return base.UseBufferedInputStream(hostContext);

        return false;
    }
}

config.Services.Replace(typeof(IHostBufferPolicySelector), new CustomBufferPolicySelector());

Just FYI...the above policy selector works only for Web Host currently. If you would like to do a similar test in SelfHost, then do the following:

//NOTE: by default, the transfer mode is TransferMode.Buffered
config.TransferMode = System.ServiceModel.TransferMode.StreamedRequest;

After Update B of the post above:

You could modify your handler like below:

public class LoggingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Content != null)
        {
            string requestContent = await request.Content.ReadAsStringAsync();
        }

        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

        if (response.Content != null)
        {
            string responseContent = await response.Content.ReadAsStringAsync();
        }

        return response;
    }
}
Kiran
  • 56,921
  • 15
  • 176
  • 161
  • I was thrown by that comment by Filip. I did use ReadAsStringAsync and my model would be null. Here's the basic code I used in the ITraceWriter implementation: request.Content.ReadAsStringAsync().ContinueWith(s => { string requestText = s.Result; Logger.Log(requestText); }); – Bryan Mar 19 '13 at 20:42
  • I am unable to repro the issue you are mentioning. For example (not the best way to do it), i have the following code in the WriteTrace method of SimpleTracer from Mike Wasson's sample: if (rec.Request != null){ Console.WriteLine(rec.Category + ", " + rec.Request.Content.ReadAsStringAsync().Result); } – Kiran Mar 19 '13 at 23:48
  • Thanks for trying to repro. Trying to figure out what's different. I'm using MVC 4 and running in IIS Express for my local dev. Maybe IIS Express is the difference. I'm trying something different and will post back. – Bryan Mar 20 '13 at 02:46
  • I tried following this: http://weblogs.asp.net/pglavich/archive/2012/02/26/asp-net-web-api-request-response-usage-logging.aspx which like Filip's example uses a DelegatingHandler. I don't know if it is a timing issue or what, but some calls to a method from test harness that makes multiple calls seem to work and some seem to end up with a null model. Also, a side note: trying to follow Filip's very simple example directly he uses and await which means the method has an async so it forces me to return response.Result, not just response. I may need to just create a simpler example to test. – Bryan Mar 20 '13 at 03:14
  • I updated the question with more details and a sample. If you have a chance and can try to repro with the sample code or offer any further insight I'd appreciate it. – Bryan Mar 22 '13 at 01:59
  • right, previously you were not waiting for the accessing the result. I have currently updated my post with the async-await version. you can try and see if it works fine. – Kiran Mar 22 '13 at 02:34
  • Just wanted to say thanks! I was having this same frustration. Pity I only found this now after 2 days struggling. – Talon Oct 03 '13 at 15:33