首页 > 解决方案 > 如何在 InvalidModelStateResponseFactory 中获取请求正文?

问题描述

我有一个由外部应用程序调用的 API,我相信我们有很多 400 错误。

我查找了如何捕获和记录自动 HTTP 400 错误,并找到了以下解决方案:

services.AddMvc()
        .ConfigureApiBehaviorOptions(options =>
        {
            options.InvalidModelStateResponseFactory = context =>
            {
                ILogger logger = context.HttpContext.RequestServices
                                            .GetRequiredService<ILogger>();
                string keys = JsonConvert.SerializeObject(context.ModelState.Keys);
                string values = JsonConvert.SerializeObject(context.ModelState.Values);

                logger.Debug($"Keys: {keys}, Values: {values}");

                return new BadRequestObjectResult(context.ModelState);
            };
        });

这很好用,但我假设我会得到所有键和所有值,而实际上,它只显示有错误的键,甚至不显示它们发送的值。

有没有办法记录整个请求正文,InvalidModelStateResponseFactory或者我以错误的方式查看它?

请注意,我不想使用该SuppressModelStateInvalidFilter选项。

标签: c#asp.net-core-mvcasp.net-core-webapiasp.net-core-3.1modelstate

解决方案


多次读取请求正文需要缓冲并将其存储在某处,而不仅仅是在它到达时进行流式传输(记住模型绑定器已经读取过一次)。请注意,这样做会对性能产生负面影响,因为请求需要存储在内存中(较大的请求在磁盘上)。我选择仅在非生产实例中启用此功能。这是我如何让它工作的:

首先,您需要告诉请求您将多次需要它。这是我改编自这个答案的中间件

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; 

public sealed class RequestBodyRewindMiddleware
{
    readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        try { context.Request.EnableBuffering(); } catch { }
        await _next(context);
    }
}

public static class BodyRewindExtensions
{
    public static IApplicationBuilder EnableRequestBodyRewind(this IApplicationBuilder app)
    {
        if (app is null)
            throw new ArgumentNullException(nameof(app));

        return app.UseMiddleware<RequestBodyRewindMiddleware>();
    }
}

我是这样注册的:

   public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (!env.IsProduction())
            app.EnableRequestBodyRewind();
    }

最后是您要查找的代码:

public void ConfigureServices(IServiceCollection services)
{   
    services.PostConfigure<ApiBehaviorOptions>(opt =>
    {
        var defaultFactory = opt.InvalidModelStateResponseFactory;
        opt.InvalidModelStateResponseFactory = context =>
        {
            AllowSynchronousIO(context.HttpContext);

            var result = defaultFactory(context);

            var bad = result as BadRequestObjectResult;                    
            if (bad?.Value is ValidationProblemDetails problem)
                LogInvalidModelState(context, problem);

            return result;

            static void AllowSynchronousIO(HttpContext httpContext)
            { 
                IHttpBodyControlFeature? maybeSyncIoFeature = httpContext.Features.Get<IHttpBodyControlFeature>();
                if (maybeSyncIoFeature is IHttpBodyControlFeature syncIoFeature)
                    syncIoFeature.AllowSynchronousIO = true;
            }

            static void LogInvalidModelState(ActionContext actionContext, ValidationProblemDetails error)
            {
                var errorJson = System.Text.Json.JsonSerializer.Serialize(error);

                var reqBody = actionContext.HttpContext.Request.Body;
                if (reqBody.CanSeek) reqBody.Position = 0;
                var sr = new System.IO.StreamReader(reqBody);
                string body = sr.ReadToEnd();

                actionContext.HttpContext
                    .RequestServices.GetRequiredService<ILoggerFactory>()
                    .CreateLogger(nameof(ApiBehaviorOptions.InvalidModelStateResponseFactory))
                    .LogWarning("Invalid model state. Responded: '{ValidationProblemDetails}'. Received: '{Request}'", errorJson, body);
            }
        };
    });
}

由于我们没有提供异步工厂功能的选项,并且默认情况下禁用同步读取,因此我们必须为此请求显式启用它。代码来自此处的公告问题。

确保在阅读正文流之前倒带它,并且不要丢弃它以防管道中的其他东西需要它。

在我的情况下,中间件未在生产中注册,CanSeek将是false并且StreamReader.ReadToEnd只返回一个空字符串。


推荐阅读