11

I'm trying to access a request's raw input body/stream in ASP.net 5. In the past, I was able to reset the position of the input stream to 0 and read it into a memory stream but when I attempt to do this from the context the input stream is either null or throws an error (System.NotSupportedException => "Specified method is not supported.").

In the first example below I can access the raw request in a controller if I declare the controller method's parameter object type as dynamic. For various reasons, this is not a solution and I need to access the raw request body in an authentication filter anyways.

This Example Works, But Is Not a Reasonable Solution:

[HttpPost("requestme")]
public string GetRequestBody([FromBody] dynamic body)
{   
    return body.ToString();
}

Throws Error:

[HttpPost("requestme")]
public string GetRequestBody()
{
    var m = new MemoryStream();
    Request.Body.CopyTo(m);

    var contentLength = m.Length;

    var b = System.Text.Encoding.UTF8.GetString(m.ToArray());

    return b;
}

Throws Error:

[HttpPost("requestme")]
public string GetRequestBody()
{
    Request.Body.Position = 0;
    var input = new StreamReader(Request.Body).ReadToEnd();

    return input;
}

Throws Error:

[HttpPost("requestme")]
public string GetRequestBody()
{
    Request.Body.Position = 0;
    var input = new MemoryStream();
    Request.Body.CopyTo(input);

    var inputString = System.Text.Encoding.UTF8.GetString(input.ToArray());

    return inputString;
}

I need to access the raw request body of every request that comes in for an API that I am building.

Any help or direction would be greatly appreciated!

EDIT:

Here is the code that I would like to read the request body in.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Http;

namespace API.Filters
{
    public class CustomAuthorizationAttribute : Attribute, IAuthorizationFilter
    {
        public CustomAuthorizationAttribute()
        { }

        public void OnAuthorization(AuthorizationContext context)
        {
            if (context == null)
                throw new ArgumentNullException("OnAuthorization AuthorizationContext context can not be null.");
            else
            {
                if (this.AuthorizeCore(context.HttpContext) == false)
                {
                    // Do Other Stuff To Check Auth
                }
                else
                {
                    context.Result = new HttpUnauthorizedResult();
                }
            }
        }

        protected virtual bool AuthorizeCore(HttpContext httpContext)
        {
            var result = false;

            using (System.IO.MemoryStream m = new System.IO.MemoryStream())
            {
                try
                {
                    if (httpContext.Request.Body.CanSeek == true)
                        httpContext.Request.Body.Position = 0;

                    httpContext.Request.Body.CopyTo(m);

                    var bodyString = System.Text.Encoding.UTF8.GetString(m.ToArray());

                    return CheckBody(bodyString); // Initial Auth Check returns true/false <-- Not Shown In Code Here on Stack Overflow
                }
                catch (Exception ex)
                {
                    Logger.WriteLine(ex.Message);
                }
            }
                return false;
        }
    }
}

This code would be accessed when a call is made to a controller method marked with the CustomAuthorization attribute like so.

[Filters.CustomAuthorizationAuthorization]
[HttpPost]
public ActionResult Post([FromBody]UserModel Profile)
{
    // Process Profile
}
Set
  • 40,147
  • 18
  • 114
  • 133
Badger Dev
  • 119
  • 1
  • 1
  • 6
  • 1
    What is the error that is being thrown? – haim770 Jul 28 '15 at 14:18
  • _I need to access the raw request body in an authentication filter anyways_ ... this q&a may be helpful http://stackoverflow.com/q/31464359/571637 – jltrem Jul 28 '15 at 14:37
  • @haim770 the error is the top of my post its System.NotSupportedException Message -> "Specified method is not supported." – Badger Dev Jul 28 '15 at 16:44

4 Answers4

10

The implementation of Request.Body depends on the controller action.

If the action contains parameters it's implemented by Microsoft.AspNet.WebUtilities.FileBufferingReadStream, which supports seeking (Request.Body.CanSeek == true). This type also supports setting the Request.Body.Position.

However, if your action contains no parameters it's implemented by Microsoft.AspNet.Loader.IIS.FeatureModel.RequestBody, which does not support seeking (Request.Body.CanSeek == false). This means you can not adjust the Position property and you can just start reading the stream.

This difference probably has to do with the fact that MVC needs to extract the parameters values from the request body, therefore it needs to read the request.

In your case, your action does not have any parameters. So the Microsoft.AspNet.Loader.IIS.FeatureModel.RequestBody is used, which throws an exception if you try to set the Position property.


Solution: either do not set the position or check if you actually can set the position first:
if (Request.Body.CanSeek)
{
    // Reset the position to zero to read from the beginning.
    Request.Body.Position = 0;
}

var input = new StreamReader(Request.Body).ReadToEnd();
Henk Mollema
  • 36,611
  • 11
  • 79
  • 100
  • Hi Henk! Thanks for the help! Do you know how I would go about this in an attribute Filter? When I run the same code in a filter it errors out in the same manner. The context in which I am trying to ready the raw input body is from an AuthorizationContext. – Badger Dev Jul 28 '15 at 14:41
  • @BadgerDev can you show the code in the filter? And what exception do you receive? – Henk Mollema Jul 28 '15 at 14:43
  • That's an interesting answer, though you're assuming the OP is using IIS. The same remarks can indeed apply to every stream host like IIS or WebListener. – Kévin Chalet Jul 28 '15 at 15:19
  • I am hosting the final app on IIS 8.5 in a DNX. That could change at some point as things can be fluid around here. @HenkMollema I edited the original post to include an example of the code that I want to run. I tried your example but for some reason the debugger in VS 2015 is just skipping over it. – Badger Dev Jul 28 '15 at 16:45
  • Just a followup. It doesn't matter if the request has parameters or not. I haven't seen a request come in where I can set the position of the stream and on every request when I have tried to read the stream regardless of its position, the stream is basically empty. There has to be something in the AspNet pipeline that is emptying the stream or not copying back the stream after it works with it. – Badger Dev Aug 04 '15 at 14:56
8

The exceptions you see in your three last snippets are the direct consequence of trying to read the request body multiple times - once by MVC 6 and once in your custom code - when using a streamed host like IIS or WebListener. You can see this SO question for more information: Read body twice in Asp.Net 5.

That said, I'd only expect this to happen when using application/x-www-form-urlencoded, since it wouldn't be safe for MVC to start reading the request stream with lengthy requests like file uploads. If that's not the case, then it's probably a MVC bug you should report on https://github.com/aspnet/Mvc.

For workarounds, you should take a look at this SO answer, that explains how you can use context.Request.ReadFormAsync or add manual buffering: Read body twice in Asp.Net 5

app.Use(next => async context => {
    // Keep the original stream in a separate
    // variable to restore it later if necessary.
    var stream = context.Request.Body;

    // Optimization: don't buffer the request if
    // there was no stream or if it is rewindable.
    if (stream == Stream.Null || stream.CanSeek) {
        await next(context);

        return;
    }

    try {
        using (var buffer = new MemoryStream()) {
            // Copy the request stream to the memory stream.
            await stream.CopyToAsync(buffer);

            // Rewind the memory stream.
            buffer.Position = 0L;

            // Replace the request stream by the memory stream.
            context.Request.Body = buffer;

            // Invoke the rest of the pipeline.
            await next(context);
        }
    }

    finally {
        // Restore the original stream.
        context.Request.Body = stream;
    }
});
Community
  • 1
  • 1
Kévin Chalet
  • 33,128
  • 7
  • 104
  • 124
2

I just had this same issue. Remove the parameters from the method signature, and then read the Request.Body Stream how you want to.

joe
  • 1,065
  • 9
  • 18
  • 1
    This worked a treat. I had a 3rd party performing callbacks with the wrong content type which meant my method consistently logged nothing- implementing this allowed me to log whatever trash was thrown at my end point! Cheers. – Shawson Apr 04 '17 at 13:29
0

You need to call Request.EnableRewind() to allow the stream to be rewound so you can read it.

string bodyAsString;
Request.EnableRewind();
using (var streamReader = new StreamReader(Request.Body, Encoding.UTF8))
{
    bodyAsString = streamReader.ReadToEnd();
}
JHobern
  • 858
  • 1
  • 12
  • 19