如何在 ASP .NET Core 最小 API 中使用 415(不支持的媒体类型)上的 JSON 正文进行响应?

How To Respond With a JSON Body on 415 (Unsupported Media Type) in ASP .NET Core Minimal APIs?

提问人:Christian Findlay 提问时间:9/25/2023 最后编辑:Christian Findlay 更新时间:9/27/2023 访问量:434

问:

如果调用没有标头的 ASP .NET Core 最小 API 终结点,则会收到状态代码为 415 的空正文响应。我需要处理这个错误并用一些错误JSON进行响应。我无法让终结点使用空字符串以外的任何内容进行响应。此外,异常处理程序不会截获任何内容。Content-Type

下面是一个示例应用和测试,用于解释该问题。

应用程序接口


using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc;

[assembly: InternalsVisibleTo("TestProject1")]

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<RouteHandlerOptions>(o => o.ThrowOnBadRequest = true);

var app = builder.Build();

app.MapPost("/hi", ([FromBody] Request request) => "Hello World!");
app.MapPost("/throwerror", ([FromBody] Request request) => true ? throw new Exception("Ouch") : "");

app.UseExceptionHandler(handlerApp => handlerApp.Run(async context =>
{
    context.Response.StatusCode = 400;
    await context.Response.WriteAsync("Threw Error");
}));

app.Run();

record Request(int Id);

测试

using System.Text;
using Microsoft.AspNetCore.Mvc.Testing;
namespace TestProject1;

[TestClass]
public class UnitTest1
{
    //This test doesn't pass. I think it should hit the exception handler and set the body to "Threw Error"
    //What I need to achieve is the ability to give a meaningful error message in the body if the consumer
    //doesn't specify a Content-Type header 
    [TestMethod]
    public async Task TestHiRespondsWithTextFromExceptionHandlerOnNoContentType()
    {
        var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder => { });
        var client = factory.CreateClient();
        var response = await client.PostAsync(new Uri("http://localhost/hi"), 
            //No content type
            new StringContent( "{ \"Id\": \"123\" }", Encoding.UTF8));
        var body = await response.Content.ReadAsStringAsync();
        //this would prove the exception handler is handling this
        Assert.AreEqual("Threw Error", body);
    }
    
    [TestMethod]
    public async Task TestSuccessRespondsWithHelloWorld()
    {
        var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder => { });
        var client = factory.CreateClient();
        var response = await client.PostAsync(new Uri("http://localhost/hi"), 
            new StringContent( "{ \"Id\": \"123\" }", Encoding.UTF8,"application/json"));
        var body = await response.Content.ReadAsStringAsync();
        Assert.AreEqual("Hello World!", body);
    }
    
    [TestMethod]
    public async Task TestThrowErrorRespondsWithTextFromExceptionHandler()
    {
        var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder => { });
        var client = factory.CreateClient();
        var response = await client.PostAsync(new Uri("http://localhost/throwerror"), 
            new StringContent( "{ \"Id\": \"123\" }", Encoding.UTF8,"application/json"));
        var body = await response.Content.ReadAsStringAsync();
        //this proves the exception handler is handling this
        Assert.AreEqual("Threw Error", body);
    }
}

请注意,第一个测试失败,因为它从未命中异常处理程序。就好像有一些中间件在任何其他中间件之前执行,并且它永远不会抛出异常。它只是在给我做任何事情的机会之前用 415 回应。如何挂接到这个中间件以使用正文进行响应?Unsupported Media Type

底部的两个测试通过。

C# .NET ASP.net-core 异常 minimal-API

评论

0赞 Panagiotis Kanavos 9/27/2023
你为什么想要这个?目前的行为比你要求的更具体、更便宜。该请求在 API 开始处理之前就被 ASP.NET Core 拒绝,甚至可能在它开始读取正文之前。这是一件好事 - 您不想浪费 CPU 和 RAM 来处理坏消息。这不仅仅是 400(错误请求),而是不支持的媒体类型
0赞 Panagiotis Kanavos 9/27/2023
您可以通过 ConfigureApiBehaviorOptions 中的 ApiBehaviorOptions.ClientErrorMapping 属性更改 API 行为,将 415 映射到您自己的 ClientErrorData 内容

答:

2赞 Meligy 9/25/2023 #1

您可以尝试添加如下自定义中间件:

public class CustomUnsupportedMediaTypeMiddleware
{
    private readonly RequestDelegate _next;

    public CustomUnsupportedMediaTypeMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        int statusCode;
        try
        {
            await _next(context);
            // 405 does not throw an exception
            statusCode = context.Response.StatusCode;
        }
        // `context.Response.StatusCode` is not set yet, it'll still be 200 here and in `finally`
        catch
        {
            // log, etc
            statusCode = 500;
        }

        // if (context.Response.StatusCode == 415)
        if (statusCode > 400)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("Threw Error");
        }
    }
}

并在路线之前使用它,例如:

var app = builder.Build();

app.UseMiddleware<CustomUnsupportedMediaTypeMiddleware>();

app.MapPost("/hi", ([FromBody] Request request) => "Hello World!");
app.MapPost("/throwerror", ([FromBody] Request request) => true ? throw new Exception("Ouch") : "");

app.Run();

我从 ChatGPT 那里得到了最初的想法: https://chat.openai.com/share/8784c09d-d068-4214-b40d-be7117c877a3
但这还不够,因为:
Ouch

  • 415 不会抛出异常,只是设置状态代码
  • 自定义异常(case)未设置设置状态代码,只会抛出异常ouch

所以,我用它来使所有测试都通过:

Tests passing

更新

OP @christian-findlay 在回答评论中要求我将其更改为该方法,因为它在新的 API 世界中更传统,根据文档app.Use

因此,中间件的使用和 API 路由的创建是:

var app = builder.Build();

// app.UseMiddleware<CustomUnsupportedMediaTypeMiddleware>();
app.Use(OverrideErrors);

app.MapPost("/hi", ([FromBody] Request request) => "Hello World!");
app.MapPost("/throwerror", ([FromBody] Request request) => true ? throw new Exception("Ouch") : "");

app.Run();

中间的实现方式几乎相同:OverrideErrors

async Task OverrideErrors(HttpContext context, RequestDelegate next)
{
    int statusCode;
    try
    {
        await next(context);
        // 405 does not throw an exception
        statusCode = context.Response.StatusCode;
    }
    // `context.Response.StatusCode` is not set yet, it'll still be 200 here and in `finally`
    catch
    {
        // log, etc
        statusCode = 500;
    }

    // if (context.Response.StatusCode == 415)
    if (statusCode > 400)
    {
        context.Response.StatusCode = 400;
        await context.Response.WriteAsync("Threw Error");
    }
};

此特定代码的唯一区别是,我们的函数需要在记录之前定义,否则 C# 会抱怨。Request

它仍然有效:

app Use success

评论

0赞 Christian Findlay 9/27/2023
可以确认这有效。我最终使用了 Use() 扩展而不是 MVC 样式的 UseMiddleware 扩展。你有没有机会链接到这个文档(learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/...)并添加一个注释,你可以使用这些方法中的任何一种?您还需要像在这里一样等待 next(context)。
1赞 Meligy 9/27/2023
@ChristianFindlay 说得好。做。请查看答案并检查它是否更正确。
1赞 Christian Findlay 10/1/2023
很棒的工作,非常有帮助。谢谢