带有 ReturnHttpNotAcceptable 和自定义媒体类型的 ASP.NET Core Web API 在 4xx 响应中导致“找不到输出格式化程序”

问题描述

我有一个 ASP.NET Core Web API(TargetFramework 是 net5.0),我接受 2 种媒体类型

  • application/json
  • application/vnd.my.customtype+json

我有一个基本控制器,如下所示:

[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("")]
public class RootController : ControllerBase
{
    [HttpGet]
    [HttpHead]
    [Route("",Name = "get_root")]
    [ApiConventionMethod(typeof(DefaultApiConventions),nameof(DefaultApiConventions.Get))]
    public IActionResult Get()
    {
        return this.Ok(new { Item1 = "item 1" });
    }
}

如果我使用标头 GET 发出 Accept: application/json 请求并返回带有结果的 200 OK,则一切正常:

GET https://localhost:5001/ HTTP/1.1
Accept: application/json
Host: localhost:5001

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 18

{"item1":"item 1"}

如果我使用标头 GET 发出 Accept: application/vnd.my.customtype+json 请求并返回带有结果的 200 OK,一切也都很好:

GET https://localhost:5001/ HTTP/1.1
Accept: application/vnd.my.customtype+json
Host: localhost:5001

HTTP/1.1 200 OK
Content-Type: application/vnd.my.customtype+json; charset=utf-8
Server: Kestrel
Content-Length: 18

{"item1":"item 1"}

返回 404 的相同控制器(例如,如果未找到资源)

[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("")]
public class RootController : ControllerBase
{
    [HttpGet]
    [HttpHead]
    [Route("",nameof(DefaultApiConventions.Get))]
    public IActionResult Get()
    {
        return this.NotFound();
    }
}

现在,如果我使用标头 GET 发出 Accept: application/json 请求,一切正常(我在响应正文中得到正确的 404 和问题详细信息):

GET https://localhost:5001/ HTTP/1.1
Accept: application/json
Host: localhost:5001

HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
Server: Kestrel
Content-Length: 161

{
  "type":"https://tools.ietf.org/html/rfc7231#section-6.5.4","title":"Not Found","status":404,"traceId":"00-54d50dfe6a27674ba83a73324fcf2721-d4286734f860c94a-00"
}

如果我使用标头 GET 发出 Accept: vnd.my.customtype+json 请求,我会收到 406 Not Acceptable 响应:

GET https://localhost:5001/ HTTP/1.1
Accept: application/vnd.my.customtype+json
Host: localhost:5001

HTTP/1.1 406 Not Acceptable
Server: Kestrel
Content-Length: 0

在日志中,我得到了这个:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:5001/ - -

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'Hooli.WebAPI.Controllers.RootController.Get (Hooli.WebAPI)'

info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
      Route matched with {action = "Get",controller = "Root"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult Get() on controller Hooli.WebAPI.Controllers.RootController (Hooli.WebAPI).

warn: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
      No output formatter was found for content types 'application/problem+json,application/problem+xml' to write the response.

info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
      Executed action Hooli.WebAPI.Controllers.RootController.Get (Hooli.WebAPI) in 0.8058ms

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'Hooli.WebAPI.Controllers.RootController.Get (Hooli.WebAPI)'

info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 GET https://localhost:5001/ - - - 406 0 - 1.1703ms

注意 warn 行:No output formatter was found for content types 'application/problem+json,application/problem+xml' to write the response.

我不明白为什么这不会返回带有问题详细信息的正确 404

我的Startup.cs如下:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        this.Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers(
                     options =>
                     {
                         options.ReturnHttpNotAcceptable = true;
                     })
                .AddNewtonsoftJson(
                     options =>
                     {
                         options.SerializerSettings.ContractResolver =
                             new CamelCasePropertyNamesContractResolver
                             {
                                 NamingStrategy = new CamelCaseNamingStrategy(true,true)
                             };
                     });

        services.Configure<Mvcoptions>(
            options =>
            {
                NewtonsoftJsonOutputFormatter newtonsoftJsonOutputFormatter =
                    options.OutputFormatters.OfType<NewtonsoftJsonOutputFormatter>().FirstOrDefault() ??
                    throw new ArgumentNullException(nameof(newtonsoftJsonOutputFormatter));

                // add media types that we want to support by default
                newtonsoftJsonOutputFormatter
                    .SupportedMediaTypes
                    .Add(MediaTypeHeaderValue.Parse("application/vnd.my.customtype+json"));
            });
    }

    public void Configure(IApplicationBuilder app,IWebHostEnvironment env)
    {
        app.UseExceptionHandler(env.IsDevelopment() ? "/error-local-development" : "/error");
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }
}

为了完整性,我的 Program.cs 文件

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args)
    {
        return Host.CreateDefaultBuilder(args)
                   .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
    }
}

我已经将我的 API 精简到这个最基本的设置,以排除任何额外的中间件/配置。我返回什么状态代码并不重要,如果它是任何 4xx 响应,当 406 标头是自定义媒体类型时,我总是得到 Accept

例如

  • this.BadRequest();
  • this.NotFound();
  • this.Unauthorized();

如果我将 options.ReturnHttpNotAcceptable = true 更改为 options.ReturnHttpNotAcceptable = false,所有响应都会正常返回,但是当在 Accept 中传递无效媒体类型时,API 不会返回 406标题

自定义媒体类型位于 Accept 标头中时,如何让 API 返回正确的响应?

解决方法

试试这个:

添加一个类:

merged_df = workers_df.merge(rates_gp,how='inner',on='state')
merged_df

    Employeeid  state   salary  rate
0   11          AL      2000    0.0046
1   12          AL      3500    0.0046
2   13          AZ      1100    0.0033
3   14          AZ      4200    0.0033

并在您的服务中注册:

public class CustomOutputFormatter: OutputFormatter
    {
        public CustomOutputFormatter()
        {
            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/vnd.my.customtype+json"));
        }
        
        public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
        {
            var response = context.HttpContext.Response;
            await response.WriteAsJsonAsync(context.Object);
        }
    }