Naftis
Naftis

Reputation: 4559

ASP.NET 5 Web API CreatedAtAction: POST returns 500

I have a simple ASP.NET 5 Web API with a controller for uploading/downloading files, like:

[Authorize(Roles = "admin,browser,writer")]
[HttpPost("api/contents/{id}")]
public IActionResult UploadContent(IFormFile file,
    [FromForm] string mimeType,
    [FromForm] string id)
{
  // store...
  return CreatedAtAction(nameof(DownloadContent), new
  {
      id = id
  });
}

[Authorize]
[HttpGet("api/contents/{id}")]
public FileResult DownloadContent([FromRoute] string id)
{
    // fetch into item...
    return File(item.Content, item.MimeType);
}

The upload action works fine, until the method returns: at this point, when the framework handles the serialization for the 201 return object (which should set its location to the corresponding download action) I get an InvalidOperationException telling that No route matches the supplied values.

I found this issue about a similar problem, but none of the mentioned solutions seem to work. I tried:

  1. explicitly naming the download action with [ActionName("DownloadContent")], even though this should not make any difference; this appears to be relevant only when the action name ends with Async (see this post, and the official issue).
  2. use CreatedAtRoute instead, by adding the corresponding Name="DownloadContent" in the HttpGet attribute of DownloadContent.
  3. use CreatedAtActionResult like:
return new CreatedAtActionResult(nameof(DownloadContent),
    nameof(ItemContentController), new { id = id }, null);
  1. directly create the URL and return a CreatedResult object:
string url = Url.Action(new UrlActionContext
{
    Protocol = Request.Scheme,
    Host = Request.Host.Value,
    Action = nameof(DownloadContent)
});
return new CreatedResult(url, null);

Now, that's the point: the above code returns null for the url, and this then makes CreatedResult throw as the url for the 201 header's Location is null. If I change this code by selecting a different action, e.g. Action = nameof(UploadContent), it works, and I get the URL for the action.

For completeness, my Startup.cs Configure method is like this:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseForwardedHeaders(new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedFor
            | ForwardedHeaders.XForwardedProto
    });
    if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
    else
    {
        app.UseExceptionHandler("/Error");
        if (Configuration.GetValue<bool>("Server:UseHSTS")) app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseCors("CorsPolicy");
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
    // ... Swagger stuff ...
}

Upvotes: 0

Views: 1420

Answers (1)

Roar S.
Roar S.

Reputation: 11339

When using overload CreatedAtAction(string, object), param object is not route parameters as you assume, but body content. Hence, code is looking for a route to GET api/contents which doesn't exists.

You'll need to use overload CreatedAtAction(string, object, object) where param #2 contains route params, and param #3 contains body content. So in your case, return CreatedAtAction(nameof(DownloadContent), new { id = id }, null); will work.

I was able to reproduce your issue with this controller and Postman:

public class MyContentController : ControllerBase
{
    [HttpGet("api/contents/{id}")]
    public IActionResult DownloadContent(int id)
    {
        return Ok(1);
    }
    
    [HttpPost("api/contents/{id}")]
    public IActionResult UploadContent(int id)
    {
        return CreatedAtAction(nameof(DownloadContent), new { id = id });
    }
}

I got expected response when replacing

return CreatedAtAction(nameof(DownloadContent), new { id = id });

with

return CreatedAtAction(nameof(DownloadContent), new { id = id }, null);

Doc: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_ControllerBase_CreatedAtAction_System_String_System_Object_

Upvotes: 1

Related Questions