asp.net-coreblazorwebassemblyasp.net-apicontroller

Blazor WASM controller: read request body causes the IIS process to crash


So I am trying to simply read the body (with string content) in a Blazor WASM ApiController. My code on the server-side:

[AllowAnonymous]
[ApiController]
[Route("[controller]")]
public class SmartMeterDataController : ControllerBase
{
    [HttpPost("UploadData")]
    public async void UploadData()
    {
        string body = null;

        if (Request.Body.CanRead && (Request.Method == HttpMethods.Post || Request.Method == HttpMethods.Put))
        {
            Request.EnableBuffering();
            Request.Body.Position = 0;
            body = await new StreamReader(Request.Body).ReadToEndAsync();
        }
    }
}

My app builder in Program.cs is pretty much out of the box:

//enable REST API controllers
            var mvcBuillder = builder.Services.AddMvcCore(setupAction: options => options.EnableEndpointRouting = false).ConfigureApiBehaviorOptions(options => //activate MVC and configure error handling
            {
                options.InvalidModelStateResponseFactory = context => //error 400 (bad request)
                {
                    JsonApiErrorHandler.HandleError400BadRequest(context);
                    return new Microsoft.AspNetCore.Mvc.BadRequestObjectResult(context.ModelState);
                };

            });

            builder.Services.AddControllersWithViews();
            builder.Services.AddRazorPages();
  ...

        app.UseRouting();
        app.UseMvcWithDefaultRoute();

        app.MapRazorPages();
        app.MapControllers();

The request body looks like this:

{"api_key":"K12345667565656", "field1":"1.10", "field2":"0.76", "field3":"0.65", "field4":"455", "field5":"0", "field6":"1324", "field7":"433761", "field8":"11815" }

Yes, this is JSON. No, I don't want to parse it with [FromBody] or similar.

POSTing to this endpoint causes the following exception (as seen in the Windows event viewer thingy):

Application: w3wp.exe
CoreCLR Version: 6.0.1222.56807
.NET Version: 6.0.12
Description: The process was terminated due to an unhandled exception.
Exception Info: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'HttpRequestStream'.
   at Microsoft.AspNetCore.Server.IIS.Core.HttpRequestStream.ValidateState(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.IIS.Core.HttpRequestStream.ReadAsync(Memory`1 destination, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.IIS.Core.WrappingStream.ReadAsync(Memory`1 destination, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
   at System.IO.StreamReader.ReadBufferAsync(CancellationToken cancellationToken)
   at System.IO.StreamReader.ReadToEndAsyncInternal()

After that, a second error is always logged. It states something like it is described here. Note that it's usually not the first, but the second or third POST that causes this. After this, the error keeps happening with every POST and after a short while the application stops working and the Windows Server 2019 need to be rebooted.

According to the internet, the code should work. Anyone have a guess why it doesn't?


Solution

  • I use this HttpContext extension method to read the request body and cache it in the context in case needed later in the pipeline. It works for me.

    Notice the condition around EnableBuffering. Perhaps adding that condition to your code will help.

    public static async Task<string> GetRequestBodyAsStringAsync(
        this HttpContext httpContext)
    {
        if (httpContext.Items.TryGetValue("BodyAsString", out object? value))
            return (string)value!;
    
        if (!httpContext.Request.Body.CanSeek)
        {
            // We only do this if the stream isn't *already* rewindable,
            // as EnableBuffering will create a new stream instance
            // each time it's called
            httpContext.Request.EnableBuffering();
        }
    
        httpContext.Request.Body.Position = 0;
    
        StreamReader reader = new(httpContext.Request.Body, Encoding.UTF8);
    
        string bodyAsString = await reader.ReadToEndAsync().ConfigureAwait(false);
    
        httpContext.Request.Body.Position = 0;
    
        httpContext.Items["BodyAsString"] = bodyAsString;
    
        return bodyAsString;
    }
    

    EDIT ...

    Possibly, your issue could also be related to fact your controller method is returning a void instead of Task?

    Finally, I found the original article I used for my extension method. Interestingly, if you that extension method for the FIRST time after model-binding then it won't work (in my project I do call it from middleware).

    https://markb.uk/asp-net-core-read-raw-request-body-as-string.html

    Adding:

    public class EnableRequestBodyBufferingMiddleware
    {
        private readonly RequestDelegate _next;
    
        public EnableRequestBodyBufferingMiddleware(RequestDelegate next) =>
            _next = next;
    
        public async Task InvokeAsync(HttpContext context)
        {
            context.Request.EnableBuffering();
    
            await _next(context);
        }
    }
    

    and

    app.UseMiddleware<EnableRequestBodyBufferingMiddleware>();
    

    may therefore also help.