Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the body of a Web API request read once?

Tags:

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.

like image 206
MichaC Avatar asked Oct 22 '12 22:10

MichaC


People also ask

What is from body in Web API?

When a parameter has [FromBody], Web API uses the Content-Type header to select a formatter. In this example, the content type is "application/json" and the request body is a raw JSON string (not a JSON object). At most one parameter is allowed to read from the message body.

How do I pass body parameters in Web API?

Use [FromUri] attribute to force Web API to get the value of complex type from the query string and [FromBody] attribute to get the value of primitive type from the request body, opposite to the default rules.

What is FromUri and FromBody in Web API?

The [FromUri] attribute is prefixed to the parameter to specify that the value should be read from the URI of the request, and the [FromBody] attribute is used to specify that the value should be read from the body of the request.


2 Answers

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.

like image 166
Filip W Avatar answered Feb 07 '23 14:02

Filip W


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!

like image 40
The Light Avatar answered Feb 07 '23 14:02

The Light