提问人:Christian Findlay 提问时间:9/25/2023 最后编辑:Christian Findlay 更新时间:9/27/2023 访问量:434
如何在 ASP .NET Core 最小 API 中使用 415(不支持的媒体类型)上的 JSON 正文进行响应?
How To Respond With a JSON Body on 415 (Unsupported Media Type) in ASP .NET Core Minimal APIs?
问:
如果调用没有标头的 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
底部的两个测试通过。
答:
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
所以,我用它来使所有测试都通过:
更新
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
它仍然有效:
评论
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
很棒的工作,非常有帮助。谢谢
评论