Request body already consumed when custom InputFormatter is called

213 views Asked by At

I'm trying to create a custom input formatter in ASP.NET Core, because there is no default or built-in formatter for form-urlencoded content. But when my formatter gets called, the body stream has already been consumed. I can verify with a middleware function that the body content is present earlier in the pipeline, so I'm not sure what's consuming it.

From the Model Binding docs:

An input formatter takes full responsibility for reading data from the request body.

Sounds nice, but in my case there's nothing left to read.

My code is very similar to the custom input formatter sample:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using System.Text;
using System.Text.Json.Nodes;

namespace slim
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddControllers(options =>
            {
                options.InputFormatters.Add(new FormUrlEncodedInputFormatter());
            });

            var app = builder.Build();
            app.MapControllers();
            // Uncomment this to see the expected body content "name=value" is present
            // app.Use(async (c, next) => 
            // {
            //     Console.WriteLine(await new StreamReader(c.Request.Body).ReadToEndAsync());
            //     await next(c);
            // });
            app.Run();
        }
    }

    [ApiController]
    public class EchoController : ControllerBase
    {
        [Route("/echo")]
        public ActionResult Echo([FromBody] JsonObject obj) => Ok(obj);
    }

    public class FormUrlEncodedInputFormatter : TextInputFormatter
    {
        public FormUrlEncodedInputFormatter()
        {
            SupportedMediaTypes.Add("application/x-www-form-urlencoded");
            SupportedEncodings.Add(Encoding.UTF8);
        }

        protected override bool CanReadType(Type type) => type == typeof(JsonObject);

        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
        {
            // PROBLEM: str is always empty ("")
            var str = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
            var d = str.Split('&').Where(s => s.Length > 0).Select(s => s.Split('='))
                .ToDictionary(kv => kv[0], kv => kv[1]);
            return InputFormatterResult.Success(d);
        }

    }
}

I'm calling the endpoint from PowerShell like this:

Invoke-RestMethod "http://localhost:5186/echo" -Method POST -Body @{ name="value" }

As a workaround, I can enable buffering for the request, which allows my formatter to rewind the body stream, but if the formatter has "full responsibility" for reading data from the request body, this shouldn't be necessary.

app.Use(async (context, next) =>
{
    if (context.Request.ContentType == "application/x-www-form-urlencoded")
    {
        context.Request.EnableBuffering();
    }
    await next(context);
});

What am I doing wrong?

UPDATE: I discovered that form-urlencoded content can be accepted by using the [FromForm] attribute, which does not appear to function as an actual InputFormatter and does not need its content type to be explicitly registered. It's possible that whatever magic makes the [FromForm] attribute work despite having no input formatter registered for the form-urlencoded content type is also responsible for consuming the request body before my formatter gets a crack at it.

The sample above works fine if you replace [FromBody] on the Echo action method parameter with [FromForm], and omit the registration of a special input formatter.

0

There are 0 answers