Getting raw POST data from Web API method - c#

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?

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()...
...

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.

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);
}

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";
}

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);

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

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();
}

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...

Related

.NET 6 web api: how to get raw the body content in controller

I'm migrating a.NET 4.8 web API application to 6.0 but the snippet used to retrieve raw body content within a controller's method no longer works and always returns an empty string.
By raw content I mean the whole json structure that is used to write a custom log.
The.net 4.8 code, which runs within a post method, is as follows:
public async Task<IActionResult> PostAsync([FromBody] FooModel model)
{
// some code
string rawContent = string.Empty;
using (var st = await this.Request.Content.ReadAsStreamAsync())
{
st.Seek(0, System.IO.SeekOrigin.Begin);
using (var sr = new System.IO.StreamReader(st))
{
rawContent = sr.ReadToEnd();
}
}
// some more code
}
In.net 6 I adapted it like this:
string rawContent = string.Empty;
using (var reader = new StreamReader(Request.Body,
encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false))
{
rawContent = await reader.ReadToEndAsync();
}
Despite some adaptations I can not recover the data. Is there anyone who can give me directions?
Thank you.
ste22
Web API application to 6.0 but the snippet used to retrieve raw body
content within a controller's method no longer works and always
returns an empty string.
Well, I have simulate the issue on dotnet 6 Web API project in following way, and I am getting the json which I am sending from PostMan
Controller:
[HttpPost]
public async Task<IActionResult> GetRawContent()
{
string rawContent = string.Empty;
using (var reader = new StreamReader(Request.Body,
encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false))
{
rawContent = await reader.ReadToEndAsync();
}
return Ok(rawContent);
}
Output:
Simulation:
Note: As you are sending raw content so you can get rid of[FromBody] FooModel model as this only used when we explicitely use stricly typed model

Can you serialize ByteArrayContent in a JSON that translates to C# object?

I have written a C# Web API in VS Studio and have created numerous DTO's that get serialized into JSON entities which any client can consume. One of my API calls is to return a PDF file, so after some online research, I set up one of the DTO's is set up in the following format. I read somewhere you can do this, but not 100% sure:
public class MyCustomResult
{
public bool Success;
public DateTime? LastRunDate;
public string ErrorCode;
public string Message;
public ByteArrayContent ReportBody;
}
I do not get any errors when return the object as an IHttpActionResult:
return Ok(result);
I can see on the server that the ByteSize of the report byte[] is approx 700K. However, when I retrieve the object on the client, the JSON entity is approx 400B and no byte content in the ByteContentStream. When I run the query in Postman, I get an empty Header, so it appears that the ByteContentStream can't be serialized by Newtonsoft JSON.
Are there any options I should consider?
Here is a scenario where you'd use ByteArrayContent:
using(var req = new HttpRequestMessage(HttpMethod.Post, new Uri("https://example.com"))
{
req.Content = new ByteArrayContent(...);
using(var resp = await _client.SendAsync(req))
{
var data = await resp.Content.ReadAsAsync<object>();
}
}
What you'd want to do is this:
public class MyCustomResult
{
public bool Success;
public DateTime? LastRunDate;
public string ErrorCode;
public string Message;
public byte[] ReportBody; // <-- change this to byte[]
}
var dataToSend = new MyCustomResult(); // fill this in
using(var req = new HttpRequestMessage(HttpMethod.Post, new Uri("https://example.com"))
{
req.Content = new StringContent(
JsonConvert.SerializeObject(dataToSend, Encoding.UTF8, "application/json"));
using(var resp = await _client.SendAsync(req))
{
var data = await resp.Content.ReadAsAsync<object>();
}
}
(note: this code is not tested)
So what will happen is SerializeObject will convert that byte array into a Base64 string then send it.
The consumer would then have to decode that Base64 string. If it's another Newtonsoft.Json client and the model definitions match, then it will automatically decode it for you.
I understand you are doing an API endpoint. The above examples are to show the use of ByteArrayContent and why it exists in .NET. How you are returning data is correct: return Ok(response); as long as you fix your model.
So to sum it up:
ByteArrayContent is an implementation of HttpContent which is supposed to be used as a response body only. It's not to be used in conjunction with a JSON response.

HttpClient POST Protobuf ByteArray to ASP.NET Core Web API

I am struggling to send some protobuf binary data to get back some other binary data.
The code is the following:
Client:
HttpClient client = new HttpClient
{
BaseAddress = new Uri("https://localhost:44302/")
};
// "Credentials" is a proto message
Credentials cred = new Credentials()
{
Email = "my#email.com",
Password = "mypassword"
};
var content = new ByteArrayContent(cred.ToByteArray());
var response = await client.PostAsync("api/database", content);
Server:
[HttpPost]
public async Task<IActionResult> Post(ByteArrayContent data)
{
Credentials c = Credentials.Parser.ParseFrom(await data.ReadAsByteArrayAsync());
// Get user from database, etc, etc...
// "user" being another proto defined message
return File(user.ToByteArray(), "application/octet-stream");
}
The thing is it doesn't even get to the server Post method. It fails directly to the client.PostAsync one. I get the Unsupported Media Type error:
I even tried the:
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
Doesn't work...
All the answers I find to this issue are either old (This is the method I'm failing to apply) or have some weird Base64 string Json encoded serialization I absolutely want to avoid...
There also are some protobuf-net related answer but I want to avoid any thirdparty package.
Ok, finally got to find the solution. Issue was in the controller. Way to do is:
[HttpPost]
public async Task<IActionResult> LogIn()
{
Credentials cred = Credentials.Parser.ParseFrom(Utils.ReadRequestBody(Request));
// Use cred...
}
with method:
public static byte[] ReadStream(in Stream input)
{
using (MemoryStream ms = new MemoryStream())
{
input.CopyTo(ms);
return ms.ToArray();
}
}
public static byte[] ReadRequestBody(in HttpRequest request)
{
using (Stream stream = request.BodyReader.AsStream()) // Package "System.IO.Pipelines" needed here
{
return ReadStream(stream);
}
}

C# How to get swagger to work when input is raw Json?

In C# how do I get swagger documentation to work, when the API is reading raw JSON? Currently as expected, it thinks the API does not take any input.
I am using the Swashbuckle.AspNetCore nuget package. Is there a way to add comment to show parameter with type so that the generated file will have the correct information?
[HttpPost]
public async Task<ActionResult<int>> Process()
{
string jsonString = await GetRequestBody();
...
}
private async Task<string> GetRequestBody()
{
string jsonString;
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
jsonString = await reader.ReadToEndAsync();
}
return jsonString;
}

How do I get the raw request body from the Request.Content object using .net 4 api endpoint

I'm trying to capture the raw request data for accountability and want to pull the request body content out of the Request object.
I've seen suggestions doing a Request.InputStream, but this method is not available on the Request object.
Any idea of how to get a string representation of the Request.Content body?
In your comment on #Kenneth's answer you're saying that ReadAsStringAsync() is returning empty string.
That's because you (or something - like model binder) already read the content, so position of internal stream in Request.Content is on the end.
What you can do is this:
public static string GetRequestBody()
{
var bodyStream = new StreamReader(HttpContext.Current.Request.InputStream);
bodyStream.BaseStream.Seek(0, SeekOrigin.Begin);
var bodyText = bodyStream.ReadToEnd();
return bodyText;
}
You can get the raw data by calling ReadAsStringAsAsync on the Request.Content property.
string result = await Request.Content.ReadAsStringAsync();
There are various overloads if you want it in a byte or in a stream. Since these are async-methods you need to make sure your controller is async:
public async Task<IHttpActionResult> GetSomething()
{
var rawMessage = await Request.Content.ReadAsStringAsync();
// ...
return Ok();
}
EDIT: if you're receiving an empty string from this method, it means something else has already read it. When it does that, it leaves the pointer at the end. An alternative method of doing this is as follows:
public IHttpActionResult GetSomething()
{
var reader = new StreamReader(Request.Body);
reader.BaseStream.Seek(0, SeekOrigin.Begin);
var rawMessage = reader.ReadToEnd();
return Ok();
}
In this case, your endpoint doesn't need to be async (unless you have other async-methods)
For other future users who do not want to make their controllers asynchronous, or cannot access the HttpContext, or are using dotnet core (this answer is the first I found on Google trying to do this), the following worked for me:
[HttpPut("{pathId}/{subPathId}"),
public IActionResult Put(int pathId, int subPathId, [FromBody] myViewModel viewModel)
{
var body = new StreamReader(Request.Body);
//The modelbinder has already read the stream and need to reset the stream index
body.BaseStream.Seek(0, SeekOrigin.Begin);
var requestBody = body.ReadToEnd();
//etc, we use this for an audit trail
}
If you need to both get the raw content from the request, but also need to use a bound model version of it in the controller, you will likely get this exception.
NotSupportedException: Specified method is not supported.
For example, your controller might look like this, leaving you wondering why the solution above doesn't work for you:
public async Task<IActionResult> Index(WebhookRequest request)
{
using var reader = new StreamReader(HttpContext.Request.Body);
// this won't fix your string empty problems
// because exception will be thrown
reader.BaseStream.Seek(0, SeekOrigin.Begin);
var body = await reader.ReadToEndAsync();
// Do stuff
}
You'll need to take your model binding out of the method parameters, and manually bind yourself:
public async Task<IActionResult> Index()
{
using var reader = new StreamReader(HttpContext.Request.Body);
// You shouldn't need this line anymore.
// reader.BaseStream.Seek(0, SeekOrigin.Begin);
// You now have the body string raw
var body = await reader.ReadToEndAsync();
// As well as a bound model
var request = JsonConvert.DeserializeObject<WebhookRequest>(body);
}
It's easy to forget this, and I've solved this issue before in the past, but just now had to relearn the solution. Hopefully my answer here will be a good reminder for myself...
Here's this answer as an extension method:
using System.IO;
using System.Text;
namespace System.Web.Http
{
public static class ApiControllerExtensions
{
public static string GetRequestBody(this ApiController controller)
{
using (var stream = new MemoryStream())
{
var context = (HttpContextBase)controller.Request.Properties["MS_HttpContext"];
context.Request.InputStream.Seek(0, SeekOrigin.Begin);
context.Request.InputStream.CopyTo(stream);
var requestBody = Encoding.UTF8.GetString(stream.ToArray());
return requestBody;
}
}
}
}

Categories