56

I have the following Web API method in an ApiController class:

public HttpResponseMessage Post([FromBody]byte[] incomingData)
{
  ...
}

I want incomingData to be the raw content of the POST. But it seems that the Web API stack attempts to parse the incoming data with the JSON formatter, and this causes the following code on the client side to fail:

new WebClient().UploadData("http://localhost:15134/api/Foo", new byte[] { 1, 2, 3 });

Is there a simple workaround for this?

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
Joe Albahari
  • 30,118
  • 7
  • 80
  • 91

8 Answers8

59

For anyone else running into this problem, the solution is to define the POST method with no parameters, and access the raw data via Request.Content:

public HttpResponseMessage Post()
{
  Request.Content.ReadAsByteArrayAsync()...
  ...
Joe Albahari
  • 30,118
  • 7
  • 80
  • 91
  • 6
    I needed something similar but for strings, and ensuring there was no parameter in the POST method ended up being essential to using the equivalent `ReadAsStringAsync`, it was empty otherwise. – Grant H. Jul 08 '14 at 23:40
  • 1
    This is the easiest way to do this I have found. Our partners keep adding new undocumented fields they want us to use and we never know exactly how they spell/format them. Thanks! – MvcCmsJon Sep 30 '15 at 16:20
  • 2
    Thanks for this. In MVC6 to get the full body, I had to remove the parameters and read `Request.Body` manually. – joe Feb 25 '16 at 04:10
  • After 4 hours fighting with "415 Unsupported Media Type" when sending an `application\octet-stream` body request, your answer helped me get it working. Thank you! – Vitox May 10 '23 at 05:04
39

If you need the raw input in addition to the model parameter for easier access, you can use the following:

using (var contentStream = await this.Request.Content.ReadAsStreamAsync())
{
    contentStream.Seek(0, SeekOrigin.Begin);
    using (var sr = new StreamReader(contentStream))
    {
        string rawContent = sr.ReadToEnd();
        // use raw content here
    }
}

The secret is using stream.Seek(0, SeekOrigin.Begin) to reset the stream before trying to read the data.

Christoph Herold
  • 1,799
  • 13
  • 18
  • Thanks for this, I have posted an answer based on your code, if you want to incorporate it feel free and I'll delete my answer. – Rocklan Jul 20 '16 at 03:40
  • System.NotSupportedException: This stream does not support seek operations. – Jeremy Holovacs Aug 09 '19 at 13:28
  • What environment are you using? – Christoph Herold Aug 09 '19 at 14:44
  • @ChristophHerold I'm getting the NotSupportedException error as Jerermy is. I'm .NET 4.7.2 with Web API MVC (Not Core). Also, my rawContent is always an empty no matter what. Are you aware of anything that could be restricting this from being read? My model is set as expected, so this is driving nuts as the content is clearly there and the content-size is also set correctly but can't grasp why it's always empty and wondering if anything in the web.config or similar could potentially block this? I thought it was because I was trying this in an attribute class but not the case. – Thierry Nov 08 '19 at 16:19
  • @Thierry I just created a simple example .NET 4.7.2 Web API MVC project, and in the `ValuesController`, I added my code to the `Post` method. For me, it works like a charm. There must be something else, that's interfering in your application. The type of stream I get from the `.ReadAsStreamAsync` call is `System.Net.Http.StreamContent.ReadOnlyStream`. Maybe, you can check what type of stream you are getting. This could help narrow things down. – Christoph Herold Nov 08 '19 at 16:38
  • @ChristophHerold I will try same over the weekend and see if I can reproduce it as you did on an empty project. For now, I've put a dirty hack in my attribute which is serialize any objects in actionContext.ModelState. Initial hit on one of our largest object is 50ms (blah!) but every other call after is <= 1ms. Odd by the way as I found: https://stackoverflow.com/questions/21351617/web-api-request-content-is-empty-in-action-filter, which say what you're doing won't work because object was consumed to create model, yet below popular answer, it states the opposite. – Thierry Nov 08 '19 at 16:48
  • @Thierry Interesting. My guess is, that it depends on the actual content being sent. For large content, or maybe even multipart content, I would want to think, the .NET infrastructure helps in conserving memory by not storing all the content, but instead only allowing you to stream it once. This would, of course, lead to the encountered behavior. This would then make Joe's answer the correct one, since the stream would not be rewindable, and the binder would already have read everything. But, this is just guesswork. – Christoph Herold Nov 08 '19 at 16:55
20

The other answers suggest removing the input parameter, but that will break all of your existing code. To answer the question properly, an easier solution is to create a function that looks like this (Thanks to Christoph below for this code):

private async Task<String> getRawPostData()
{
    using (var contentStream = await this.Request.Content.ReadAsStreamAsync())
    {
        contentStream.Seek(0, SeekOrigin.Begin);
        using (var sr = new StreamReader(contentStream))
        {
            return sr.ReadToEnd();
        }
    }
}

and then get the raw posted data inside your web api call like so:

public HttpResponseMessage Post ([FromBody]byte[] incomingData)
{
    string rawData = getRawPostData().Result;

    // log it or whatever

    return Request.CreateResponse(HttpStatusCode.OK);
}
Rocklan
  • 7,888
  • 3
  • 34
  • 49
  • 1
    Nice work, thank you! I'm adding an answer that regurgitates yours as a utility class with a static method. – William T. Mallard Nov 15 '16 at 21:18
  • If you access the Task.Result property like that you immediately do a wait on the thread. Is there any benefit to using a Task here? – codinglifestyle Sep 25 '17 at 16:55
  • @codinglifestyle I think you have to return a Task because you're calling ReadAsStreamAsync() which is an async method. – Rocklan Sep 26 '17 at 00:07
  • @Rocklan I'm getting: "Specified method is not supported" on the .Seek call, so must be version specific. – Thierry Nov 08 '19 at 15:58
  • 1
    One thing worth pointing out is that, as written, you can call the method once. If you run it more than once you will get an exception because the object has been disposed. Not a big deal, but worth mentioning. – jorge cordero Aug 12 '22 at 15:54
15

In MVC 6 Request doesn't seem to have a 'Content' property. Here's what I ended up doing:

[HttpPost]
public async Task<string> Post()
{
    string content = await new StreamReader(Request.Body).ReadToEndAsync();
    return "SUCCESS";
}
Jason Goemaat
  • 28,692
  • 15
  • 86
  • 113
11

I took LachlanB's answer and put it in a utility class with a single static method that I can use in all my controllers.

public class RawContentReader
{
    public static async Task<string> Read(HttpRequestMessage req)
    {
        using (var contentStream = await req.Content.ReadAsStreamAsync())
        {
            contentStream.Seek(0, SeekOrigin.Begin);
            using (var sr = new StreamReader(contentStream))
            {
                return sr.ReadToEnd();
            }
        }
    }
}

Then I can call it from any of my ApiController's methods this way:

string raw = await RawContentReader.Read(this.Request);
William T. Mallard
  • 1,562
  • 2
  • 25
  • 33
1

Or simply

string rawContent = await Request.Content.ReadAsStringAsync();

make sure you run the above line on THE SAME THREAD before the original request is DISPOSED

Note: This is for ASP.NET MVC 5

Adel Mourad
  • 1,351
  • 16
  • 13
0

in the controller below codeBlock gets the request body raw payload

 string requestBody = string.Empty;
        using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
        {
            requestBody = await reader.ReadToEndAsync();
        }
rajquest
  • 535
  • 1
  • 5
  • 10
0

In .NET 6 i've done something like this:

JsonObject? requestBodyJSON = await context.Request.ReadFromJsonAsync<JsonObject>();
string rawRequestBody = requestBodyJSON.ToJsonString();

So basically read body as JSON and deserialize it in JsonObject, and after that just call .ToJsonString() and you get raw body data. Good thing is that from this JsonObject you can parse it to all basic types like Dictionary, List, Array, etc...

Hazaaa
  • 144
  • 2
  • 14