24

My goal is to authenticate Web API requests using a AuthorizationFilter or DelegatingHandler. I want to look for the client id and authentication token in a few places, including the request body. At first it seemed like this would be easy, I could do something like this

var task = _message.Content.ReadAsAsync<Credentials>();

task.Wait();

if (task.Result != null)
{
    // check if credentials are valid
}

The problem is that the HttpContent can only be read once. If I do this in a Handler or a Filter then the content isn't available for me in my action method. I found a few answers here on StackOverflow, like this one: Read HttpContent in WebApi controller that explain that it is intentionally this way, but they don't say WHY. This seems like a pretty severe limitation that blocks me from using any of the cool Web API content parsing code in Filters or Handlers.

Is it a technical limitation? Is it trying to keep me from doing a VERY BAD THING(tm) that I'm not seeing?

POSTMORTEM:

I took a look at the source like Filip suggested. ReadAsStreamAsync returns the internal stream and there's nothing stopping you from calling Seek if the stream supports it. In my tests if I called ReadAsAsync then did this:

message.Content.ReadAsStreamAsync().ContinueWith(t => t.Result.Seek(0, SeekOrigin.Begin)).Wait();

The automatic model binding process would work fine when it hit my action method. I didn't use this though, I opted for something more direct:

var buffer = new MemoryStream(_message.Content.ReadAsByteArrayAsync().WaitFor());
var formatters = _message.GetConfiguration().Formatters;
var reader = formatters.FindReader(typeof(Credentials), _message.Content.Headers.ContentType);
var credentials = reader.ReadFromStreamAsync(typeof(Credentials), buffer, _message.Content, null).WaitFor() as Credentials;

With an extension method (I'm in .NET 4.0 with no await keyword)

public static class TaskExtensions
{
    public static T WaitFor<T>(this Task<T> task)
    {
        task.Wait();
        if (task.IsCanceled) { throw new ApplicationException(); }
        if (task.IsFaulted) { throw task.Exception; }
        return task.Result;
    }
}

One last catch, HttpContent has a hard-coded max buffer size:

internal const int DefaultMaxBufferSize = 65536;

So if your content is going to be bigger than that you'll need to manually call LoadIntoBufferAsync with a larger size before you try to call ReadAsByteArrayAsync.

Community
  • 1
  • 1
MichaC
  • 2,834
  • 2
  • 20
  • 20
  • 3
    Although you found a way to read the body more than once, did you guys found out why you can't read it twice? Because I'm really pulling my hair to find out why this restriction is there in the first place. – Maarten Kieft Nov 14 '13 at 10:35

2 Answers2

26

The answer you pointed to is not entirely accurate.

You can always read as string (ReadAsStringAsync)or as byte[] (ReadAsByteArrayAsync) as they buffer the request internally.

For example the dummy handler below:

public class MyHandler : DelegatingHandler
{
    protected override async System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        var body = await request.Content.ReadAsStringAsync();
        //deserialize from string i.e. using JSON.NET

        return base.SendAsync(request, cancellationToken);
    }
}

Same applies to byte[]:

public class MessageHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var requestMessage = await request.Content.ReadAsByteArrayAsync();
        //do something with requestMessage - but you will have to deserialize from byte[]

        return base.SendAsync(request, cancellationToken);
    }
}

Each will not cause the posted content to be null when it reaches the controller.

Filip W
  • 27,097
  • 6
  • 95
  • 82
  • Thank you for your response! I did notice this while I was doing my testing. I have two concerns around this. 1. I can also get at the stream directly using HttpContext. I was really hoping to leverage the ability in Web API to match the formatter to the media type, so that if the body is in XML, JSON or just a standard form submit I wouldn't have to behave differently. 2. The fact that these "ReadAsAsync" methods have different side effects worries me. The difference wasn't documented that I could find. Can I rely on it? – MichaC Oct 23 '12 at 01:27
  • The different behavior of HttpContent is not a feature of Web API but of System.Net.Http which shipped much earlier - I think that's why no one thought to explicitly document it for Web API. What reading as string/byte[] does under the hood is call LoadIntoBufferAsync http://msdn.microsoft.com/en-us/library/hh138085(v=vs.110), I also recommend you check symbol source for HttpContent. In other cases, like using MediaTypeFormatters, you use the stream explicitly and since you move the stream's position forward (and it's un-rewindable) the model binder would always get null from it. – Filip W Oct 23 '12 at 02:11
  • And yes you can rely on it, it's not a hack, this behavior is as designed :-) – Filip W Oct 23 '12 at 02:13
  • Thanks for the answer Filip, this really saved me a lot of time! Much appreciated :) – Alex Feb 06 '13 at 08:14
  • @FilipW If so , can you please explain when it is *not* possible to read twice ? – Royi Namir Jan 06 '15 at 12:18
2

I'd put the clientId and the authentication key in the header rather than content.

In which way, you can read them as many times as you like!

The Light
  • 26,341
  • 62
  • 176
  • 258
  • Thanks for the suggestion! I did consider headers, but it looked like I would have to make up some non-standard HTTP header parameters for that purpose and I wasn't sure how easy it would be for the various http client APIs on various platforms to support that. It's certainly possible that I dismissed that option too quickly. – MichaC May 09 '13 at 22:44
  • I agree. If its something small, a custom header is fine. If its a "whacko" string..you can also make it a base64 string. Again..if its small. – granadaCoder Jan 27 '16 at 15:39