.netfile-uploadhttp-postminimal-apis.net-8.0

Upload files to a minimal API endpoint in .NET 8


Edit: Code Repo here.

Also I'm aware that I can simply add .DisableAntiforgery(); to my API endpoint as explained here to make it work, but I'd like to do this the proper way.


I'm trying to upload a file from a Blazor component in the client side to a backend Minimal API endpoint using an Http POST call but after many failed attempts, I'm posting this question here.

I first tried this in my backend API as documented here:

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        // Do my thing
    }
});

Sent the request to the backend API using Swagger UI:

I was hit with this error: 😩

System.InvalidOperationException: Endpoint HTTP: POST /upload_many contains anti-forgery metadata, but a middleware was not found that supports anti-forgery.
Configure your application startup by adding app.UseAntiforgery() in the application startup code. If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAntiforgery() must go between them. Calls to app.UseAntiforgery() must be placed after calls to app.UseAuthentication() and app.UseAuthorization().
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.ThrowMissingAntiforgeryMiddlewareException(Endpoint endpoint)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

I then tried this.

Program.cs in my backend API:

//...
builder.Services.AddAntiforgery();
//...
var app = builder.Build();
//...
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        // Do my thing here
    }
});
//...

I was hit with this error: 😫

Microsoft.AspNetCore.Http.BadHttpRequestException: Invalid anti-forgery token found when reading parameter "IFormFileCollection myFiles" from the request body as form.
 ---> Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The required antiforgery cookie ".AspNetCore.Antiforgery.JitzV0NlXkw" is not present.
   at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery.ValidateRequestAsync(HttpContext httpContext)
   at Microsoft.AspNetCore.Antiforgery.Internal.AntiforgeryMiddleware.InvokeAwaited(HttpContext context)
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.Log.InvalidAntiforgeryToken(HttpContext httpContext, String parameterTypeName, String parameterName, Exception exception, Boolean shouldThrow)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<HandleRequestBodyAndCompileRequestDelegateForForm>g__TryReadFormAsync|102_0(HttpContext httpContext, String parameterTypeName, String parameterName, Boolean throwOnBadRequest)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass102_2.<<HandleRequestBodyAndCompileRequestDelegateForForm>b__2>d.MoveNext()
--- End of stack trace from previous location ---
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Antiforgery.Internal.AntiforgeryMiddleware.InvokeAwaited(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

I'm beginning to think that I'm not doing this right. And I couldn't find a working example in the documentation.


Solution

  • I figured this out after going through this.

    Full source code here.

    As per the docs, to be able to upload files you need an Authorization header, a client certificate, or a cookie header.

    To satisfy that requirement, I decided to pass Antiforgery token with the file upload POST request.

    Now my API endpoints looks like this:

    // Get token endpoint
    app.MapGet("antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
    {
        var tokens = forgeryService.GetAndStoreTokens(context);
        var xsrfToken = tokens.RequestToken!;
        return TypedResults.Content(xsrfToken, "text/plain");
    });
    //.RequireAuthorization(); // In a real world scenario, you'll only give this token to authorized users
    
    // Post files endpoint
    app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
    {
        foreach (var file in myFiles)
        {
            // ...
        }
    
        return TypedResults.Ok("Ayo, I got your files!");
    });
    

    So I've got 2 endpoints now:

    Upload the file

    Step 1: Get the token.

    Step 2.1: Before making the POST call to Upload endpoint, add the XSRF token you received from Step 1.

    Step 2.2: Add the file you want to upload. pickle.png in my case.

    Step 2.3: Hit send. At this point, you'll be able to make a successful call.