48

I have an ASP.NET Web API endpoint with controller action defined as follows :

[HttpPost]
public HttpResponseMessage Post([FromBody] object text)

If my post request body contains plain text ( i.e. should not be interpreted as json, xml, or any other special format ), then I thought I could just include following header to my request :

Content-Type: text/plain

However, I receive error :

No MediaTypeFormatter is available to read an object of type 'Object' from content with media type 'text/plain'.

If I change my controller action method signature to :

[HttpPost]
public HttpResponseMessage Post([FromBody] string text)

I get a slightly different error message :

No MediaTypeFormatter is available to read an object of type 'String' from content with media type 'text/plain'.
BaltoStar
  • 6,511
  • 13
  • 46
  • 67

7 Answers7

69

Actually it's a shame that web API doesn't have a MediaTypeFormatter for plain text. Here is the one I implemented. It can also be used to Post content.

public class TextMediaTypeFormatter : MediaTypeFormatter
{
    public TextMediaTypeFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
    }

    public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        var taskCompletionSource = new TaskCompletionSource<object>();
        try
        {
            var memoryStream = new MemoryStream();
            readStream.CopyTo(memoryStream);
            var s = System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());
            taskCompletionSource.SetResult(s);
        }
        catch (Exception e)
        {
            taskCompletionSource.SetException(e);
        }
        return taskCompletionSource.Task;
    }

    public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, System.Net.TransportContext transportContext, System.Threading.CancellationToken cancellationToken)
    {
        var buff = System.Text.Encoding.UTF8.GetBytes(value.ToString());
        return writeStream.WriteAsync(buff, 0, buff.Length, cancellationToken);
    }

    public override bool CanReadType(Type type)
    {
        return type == typeof(string);
    }

    public override bool CanWriteType(Type type)
    {
        return type == typeof(string);
    }
}

You need to "register" this formatter in your HttpConfig by something like that:

config.Formatters.Insert(0, new TextMediaTypeFormatter());
gwenzek
  • 2,451
  • 19
  • 19
18

Since Web API doesn't have out of box formatter for handling text/plain, some options:

  1. Modify your action to have no parameters... reason is having parameters triggers request body de-serialization. Now you can read the request content explicitly by doing await Request.Content.ReadAsStringAsync() to get the string

  2. Write a custom MediaTypeFormatter to handle 'text/plain'... it's actually simple to write in this case and you could keep the parameters on the action.

Andrew
  • 13,934
  • 8
  • 78
  • 93
Kiran Challa
  • 53,599
  • 15
  • 167
  • 151
  • Thanks for the response Kiran. Actually, my action signature has additional [FromUri] parameters, which I left out to keep my question simple. So I guess this means I can't avoid implicit de-serialization of request body ? In which case I need to write a custom MediaTypeFormatter to handle 'text/plain' .... – BaltoStar Sep 02 '14 at 23:52
  • request deserialization would only happen for the parameters which are `normally` considered to be read from body...examples: Explicitly `[FromBody]` decorated parameters, implicitly read from body(ex: complex types)...so my above answer only is for the ones meant to be read from the body...so you should be able to use `FromUri` parameters as usual...but in any case creating a custom formatter is the best approach as it fits well with Web API design.. – Kiran Challa Sep 03 '14 at 11:12
  • Is it not dangerous to provide this formatter? text/plain is a valid enctype of a HTML form with post method. Be sure to add antiforgery token to prevent cross site request forgery. – yonexbat Apr 08 '18 at 18:43
15

In ASP.NET Core 2.0 you simply do the following :-

using (var reader = new StreamReader(Request.Body))
{
      string plainText= reader.ReadToEnd();

      // Do something else

      return Ok(plainText);
}
Derek
  • 7,530
  • 9
  • 49
  • 80
14

Purified version using of gwenzek's formatter employing async/await:

public class PlainTextFormatter : MediaTypeFormatter
{
    public PlainTextFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
    }

    public override bool CanReadType(Type type) =>
        type == typeof(string);

    public override bool CanWriteType(Type type) =>
        type == typeof(string);

    public override async Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        var streamReader = new StreamReader(readStream);
        return await streamReader.ReadToEndAsync();
    }

    public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)
    {
        var streamReader = new StreamWriter(writeStream);
        await streamReader.WriteAsync((string) value);
    }
}

Please note I intentionally do not dispose StreamReader/StreamWriter, as this will dispose underlying streams and break Web Api flow.

To make use of it, register while building HttpConfiguration:

protected HttpConfiguration CreateHttpConfiguration()
{
    HttpConfiguration httpConfiguration = new HttpConfiguration();
    ...
    httpConfiguration.Formatters.Add(new PlainTextFormatter());
    ...
    return httpConfiguration;
}
Vitaliy Ulantikov
  • 8,883
  • 3
  • 53
  • 50
  • 1
    What will disposing break specifically? I found that not disposing of them caused error when handling exceptions. – Andrew Jul 05 '19 at 15:42
5

In some situations it might be simpler to let the JsonMediaTypeFormatter let do the work:

var formatter = GlobalConfiguration.Configuration.Formatters.Where(f=>f is System.Net.Http.Formatting.JsonMediaTypeFormatter).FirstOrDefault();
if (!formatter.SupportedMediaTypes.Any( mt => mt.MediaType == "text/plain" ))
    formatter.SupportedMediaTypes.Add( new MediaTypeHeaderValue( "text/plain" ) );
mmyta
  • 61
  • 1
  • 2
  • If I do this and then set the body like this: `resp.Content = new StringContent(outputString, Encoding.UTF8, "text\plain");`, will it just output the string as-is (even if it is not JSON) with the text-plain content-type? – N K Feb 26 '17 at 02:50
0

Very late to this party with a grossly simplified solution. I had success with this code in the controller method:

       public HttpResponseMessage FileHandler()
       {
        HttpResponseMessage response = new HttpResponseMessage();

        using (var reader = new StreamReader(System.Web.HttpContext.Current.Request.GetBufferedInputStream()))
        {
            string plainText = reader.ReadToEnd();
        } .....}

And on the client side, these are the Ajax options I used:

var ajaxOptions = {
url: 'api/fileupload/' + "?" + $.param({ "key": metaKey2, "File-Operation": "Remove", "removalFilePath": $removalFilePath, "Audit-Model": model, "Parent-Id": $parentId, "Audit-Id": $auditId }),
type: 'POST', processData: false, contentType: false, data: "BOB"
};
Madog
  • 1
0

Not a proper answer, but a quick 'n dirty workaround to unblock development...

It turns out that a quote-delimited string by itself is valid JSON. So if you know for sure that the content will always be very simple, you can wrap it in double quotes and call it application/json.

// TODO: Temporary, fix for production
HttpContent content = new StringContent($"\"{command}\"", UTF8Encoding.UTF8, "application/json");
Paul Williams
  • 2,385
  • 31
  • 27