14

I'm trying to extract some data out of a request in the new Asp.Net Web Api. I have a handler setup like this:

public class MyTestHandler : DelegatingHandler
{
    protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        if (request.Content.IsFormData())
        {
            request.Content.ReadAsStreamAsync().ContinueWith(x => {
                var result = "";
                using (var sr = new StreamReader(x.Result))
                {
                    result = sr.ReadToEnd();
                }
                Console.Write(result);
            });
        }

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

This is my http request:

POST http://127.0.0.1/test HTTP/1.1
Connection: Keep-Alive
Content-Length: 29
Content-Type: application/x-www-form-urlencoded
Expect: 100-continue
Host: 127.0.0.1

my_property=my_value

the problem is that no matter how I try to read the info from request.Content it's always empty. I've tried

request.Content.ReadAsStreamAsync
request.Content.ReadAsFormDataAsync
request.Content.ReadAs<FormDataCollection>

as well as

    [HttpGet,HttpPost]
    public string Index([FromBody]string my_property)
    {
        //my_property == null
        return "Test";
    }

None if it works. I cannot get the data out of the body. I'm hosting inside IIS on Windows 7 and using Fiddler to submit the request. What am I doing wrong?

Micah
  • 111,873
  • 86
  • 233
  • 325
  • It would be helpful to know if an independant client akin to wcftestclient.exe produces similar results. – P.Brian.Mackey Aug 17 '12 at 14:31
  • Tried to run that but it wont because it says my endpoint does not have any metadata. – Micah Aug 17 '12 at 14:43
  • That looks mighty strange. Is there some reason you are choosing this method of data transfer over the provided [HttpGet] and [HttpPost] methodology ? – Sam Axe Aug 17 '12 at 14:46
  • @Micah - Have you tried http://wcf.codeplex.com/wikipage?title=Introducing%20the%20WCF%20Web%20API%20Test%20Client – P.Brian.Mackey Aug 17 '12 at 14:50
  • @Dan-o Yes. I'm trying to see if there's an apikey included in the body of every request and validating it. If not then I want to throw an error. In this case the ApiKey can either be included in the querystring, a header, or the body of the request. – Micah Aug 17 '12 at 14:53
  • a GET wont have a body (afaik), and a POST to Web API typically involves json or xml. So this still looks strange to me - though I am admittedly not a Web API guru by any stretch of the imagination. – Sam Axe Aug 17 '12 at 15:07
  • The request is a POST and it's form-urlencoded. It doesn't have to be JSON or xml. – Micah Aug 17 '12 at 15:11
  • the method Index is actually called when you do POST request like above? – cuongle Aug 17 '12 at 15:25
  • yeah, it's part of the "TestController". – Micah Aug 17 '12 at 15:33
  • Similar answer here: http://stackoverflow.com/questions/12102879/httprequestmessage-content-is-lost-when-it-is-read-in-a-logging-delegatinghandle – Sando Aug 24 '12 at 07:23

6 Answers6

20

The problem is that with the Web Api the body can only be read once. I had an HTTP module running that was logging all the details of the request and was reading through the body.

Micah
  • 111,873
  • 86
  • 233
  • 325
  • so, doing this sort of logging in not possible, right ?? – sacretruth May 06 '15 at 11:30
  • 2
    It is possible, you need to read it into a buffer first, using the LoadIntoBufferAsync() method first. This makes the content still available for the base method. More from here https://weblogs.asp.net/fredriknormen/log-message-request-and-response-in-asp-net-webapi – Tom Oct 14 '16 at 12:46
7

It's ugly, but you it seems from initial tinkering that you can, in fact, replace the Content in DelegatingHandler ...

protected override Task SendAsync(
          HttpRequestMessage request,
          CancellationToken cancellationToken)
      {                    
          Stream stream = new MemoryStream();

          request.Content.ReadAsStreamAsync().Result.CopyTo(stream);
          stream.Seek(0,SeekOrigin.Begin);

          // copy off the content "for later"
          string query = new StreamReader(stream).ReadToEnd();
          stream.Seek(0,SeekOrigin.Begin);

          // if further processing depends on content type
          // go ahead and grab current value
          var contentType = request.Content.Headers.ContentType;

          request.Content = new StreamContent(stream);
          request.Content.Headers.ContentType = contentType;

          return base.SendAsync(request, cancellationToken);
     }

I have no idea if this is good form or bad (suspect bad), but .... it seems to work and follows model I've seen recommended for those that need to modify request headers and content "on the way in" with a DelegatingHandler.

Your mileage may vary substantially.

brmore
  • 896
  • 2
  • 9
  • 16
6

I based my answer on brmore's code;

This function can safe read content in any handler

private string SafeReadContentFrom(HttpRequestMessage request)
{
     var contentType = request.Content.Headers.ContentType;
     var contentInString = request.Content.ReadAsStringAsync().Result;
     request.Content = new StringContent(contentInString);
     request.Content.Headers.ContentType = contentType;
     return contentInString;
}
ulmer-morozov
  • 1,306
  • 14
  • 13
2

This works for me.

[HttpPost]
public IHttpActionResult Index(HttpRequestMessage request)
{
    var form = request.Content.ReadAsFormDataAsync().Result;
    return Ok();
}
Barbaros Alp
  • 6,405
  • 8
  • 47
  • 61
  • I have not tried this, so can't say from my own experience but still wanted to add a quick comment pointing to another answer: https://stackoverflow.com/a/17971766/2838827 It seems using this without async await could cause your application to deadlock. Use with caution. – My Stack Overfloweth Mar 22 '18 at 00:07
0

I had the same issue and finally chose not to write content in the logs. I am living with logging Content-Type and Content-Length.

But it is always a good idea to write all the content in the logs as far as possible.

But seems like with WebApi presently we cannot achieve this.

Sando
  • 667
  • 1
  • 6
  • 14
0

You can create a provider first. MultipartMemoryStreamProvider() then Request.Content.ReadAsMultipartAsync(provider); then read the content

public async Task<IHttpActionResult> Post(int id, string type)
{
    // Check if the request contains multipart/form-data.
    if(!Request.Content.IsMimeMultipartContent("form-data"))
        return BadRequest("Unsupported media type");

    try
    {
        var azureManager = new AzureManager();
        var imageManager = new ImageManager();
        var provider = new MultipartMemoryStreamProvider();

        await Request.Content.ReadAsMultipartAsync(provider);

        var assets = new List<Asset>();
        foreach (var file in provider.Contents)
        {
            var stream = await file.ReadAsStreamAsync();
            var guid = Guid.NewGuid();
            string blobName = guid.ToString();

            await azureManager.UploadAsync(blobName, stream);

            var asset = new Asset
            {
                PropertyId = id,
                FileId = guid,
                FileName = file.Headers.ContentDisposition.FileName.Trim('\"').ToLower(),
                FileSize = file.Headers.ContentLength ?? 0,
                MimeType = file.Headers.ContentType.MediaType.ToLower()
            };

            if (type == "photos")
            {
                asset.Type = AssetType.Photo;

                // Resize and crop copies to 16:9
                using (MemoryStream thumb = imageManager.ResizeImage(stream, 320, 180))
                {
                    await azureManager.UploadAsync(blobName, thumb, BlobContainers.Thumbs);
                }
                using (MemoryStream photo = imageManager.ResizeImage(stream, 1024, 576))
                {
                    await azureManager.UploadAsync(blobName, photo, BlobContainers.Photos);
                }
            }
            else
                asset.AssumeType();

            assets.Add(asset);
        }

        db.Assets.AddRange(assets);
        await db.SaveChangesAsync();

        return Ok(new { Message = "Assets uploaded ok", Assets = assets });
    }
    catch (Exception ex)
    {
        return BadRequest(ex.GetBaseException().Message);
    }
}
p.s.w.g
  • 146,324
  • 30
  • 291
  • 331
Johnny Chu
  • 899
  • 7
  • 4