Asp.Net Core 3.1 获取不到Post、Put请求的内容 System.NotSupportedException Specified method is not supported

问题

是这样的,我.net core 2.1的项目,读取、获取Post请求内容的一段代码,大概这样:

[HttpPost]
public async Task<IActionResult> Test([FromBody]string postStr)
{
    using (var reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8))
    {
        reader.BaseStream.Seek(0, SeekOrigin.Begin);  //大概是== Request.Body.Position = 0;的意思
        var readerStr = await reader.ReadToEndAsync();
        reader.BaseStream.Seek(0, SeekOrigin.Begin);  //读完后也复原
        return Ok(readerStr);
    }
}

但这段代码 在 .net core 3.1.0.net core 3.1.2(没错特地升级过) 都读不到、获取不到Post的内容:

curl --location --request POST 'http://localhost:5001/api/TestPostReader/test' \
--header 'Content-Type: application/json' \
--data-raw '{"name":"hei"}'

报异常:

System.NotSupportedException: Specified method is not supported.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.Seek(Int64 offset, SeekOrigin origin)
   at Push.WebApi.Controllers.TestPostReaderController.Test() in D:\工作\39\solution-push\Push.WebApi\Controllers\TestPostReaderController.cs:line 21
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

解决

StartUp Configure 这里改成这样:

app.Use((context, next) =>
{
    context.Request.EnableBuffering();
    return next();
});
//这个在后边
app.UseRouting();

搞定:
在这里插入图片描述


收集 ASP.NET Core 网站所有的 HTTP Request 资讯

在开发 Web API 的时候你可能会遇到这种情境,想要收集所有对我们网站所发起的 HTTP 要求,从呼叫 API 的网址、HTTP 方法、甚至 HTTP 要求的内容(Request Body)等,要把这些资讯储存下来,供之后分析使用,以前你可能会透过 IIS Log 来做,现在在 ASP.NET Core 的程式架构中,我们可以在专案架构的中介程序中,拦截 HTTP 资讯,来做任何我们想要做的事。
HTTP Request 经过中介程序的处理流程
上图是 ASP.NET Core 中介程序架构的简单表示图,而我们这次的目标像是在 ASP.NET Core 的中介程序中,加入一个我们客制的 Logging Middleware,让所有进入此应用程式的 HTTP Request 都会被我们拦截,然后记录下来。

启动 Logging 记录器

首先,我们可以在应用程式启动时,加入 ASP.NET Core 内建支援的 Logging 记录器,只要在 Program.cs 档案中设定 Logging 的机制,写法如下:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        // 加入 Logging 的机制
        .ConfigureLogging((logging) =>
        {
            logging.AddDebug();
            logging.AddConsole();
        })
        .UseStartup<Startup>();

预设这个功能会使用 appsettings.json (或开发时期使用的 appsettings.Development.json) 里面的设定值,为了让这个记录器能够将 Trace (追踪)等级的资讯记录下来,要修改这个档案 LoggingLogLevel 预设纪录层级为 Trace,如下:

{
  "Logging": {
    "LogLevel": {
      "Default": "Trace",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

客制 Logging Middleware 中介程序

重点在这里,接著我们增加一个 LoggingMiddleware.cs 中介程序,这里面将会是实作整个处理 HTTP Request 并将其记录下来的关键。

关于 ASP.NET Core 中介程序的用途说明及基本写法,请参考这篇官方文件
我们预期的动作会像下图这样,request 进入到此中介程序时,会将该 request 复制一份给 Logger 记录器做处理,同时也把一样的 request 往下传递。

Logging 中介程序的动作

为什么要复制一份呢?是因为在 ASP.NET Core 的 HttpContext 中,该 HTTP 的 Request.Body 属性是 Stream 类型,且此属性仅能被读取一次,若不将其存留起来,后续的中介程序会拿不到完整的 HTTP Request,造成应用程式异常。

因此在这里的处理有些地方需要特别注意,先看一下下面的程式码:

public async Task Invoke(HttpContext context)
{
    // 确保 HTTP Request 可以多次读取
    context.Request.EnableBuffering();
    // 读取 HTTP Request Body 内容
    // 注意!要设定 leaveOpen 属性为 true 使 StreamReader 关闭时,HTTP Request 的 Stream 不会跟著关闭
    using (var bodyReader = new StreamReader(stream: context.Request.Body,
                                              encoding: Encoding.UTF8,
                                              detectEncodingFromByteOrderMarks: false,
                                              bufferSize: 1024,
                                              leaveOpen: true))
    {
        var body = await bodyReader.ReadToEndAsync();
        var log = $"{context.Request.Path}, {context.Request.Method}, {body}";
        _logger.LogTrace(log);
    }
    // 将 HTTP Request 的 Stream 起始位置归零
    context.Request.Body.Position = 0;
    await _next.Invoke(context);
}

第一步,要确表 HTTP Request 可以被多次读取,必须要执行 context.Request.EnableBuffering(); 开启缓存的机制。

第二步,在建立 StreamReader 来存取 HTTP Request 的时候,必须在 StreamReader 建构式中设定 leaveOpen: true,使 StreamReader 的来源(Request.Body)不会因为 StreamReader 关闭 Stream 而跟著关闭,这点很重要!如果没有这样做,你的应用程式会出现类似下面这样的错误讯息:

{
    "errors": {
        "": [
            "A non-empty request body is required."
        ]
    },
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "8000001e-0001-ff00-b63f-84710c7967bb"
}

A non-empty request body is required 就是告诉你,后续要处理 HTTP Request 的中介程序,无法接受空的 Request.Body,因此出现错误。

第三步,使用 StreamReader.ReadToEndAsync() 来读取资料,读取到资料后,你可以做任何你想要做的事,例如上述情境所提到的,将所有 HTTP Request 储存记录下来。

第四步,透过 context.Request.Body.Position = 0; 将原本的 Request.BodyStream 起始位置归零。

启用客制的 Logging Middleware 中介程序

为了优雅的启用我们客制的 Logging Middleware 中介程序,在范例程式码中做了一个 LoggingMiddlewareExtensions 扩充方法:

public static class LoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<LoggingMiddleware>();
    }
}

这个扩充方法是让我们在 Startup.cs 档案的 Configure() 要启用 Logging Middleware 中介程序时,只需要使用 app.UseLoggingMiddleware(); 这样直觉的写法,就能轻松启动。

本篇完整范例程式码请参考 poychang/Demo-Logging-Http-Request

后记

这个范例的核心精神还可以延伸处理很多事情,例如我可以透过这样的处理方式,建立一个特定的 API Endpoint,只要 HTTP Request 进去这个中介程序,当网址路径符合预期的位置,就执行特定功能,在往下接续处理;或是将限制来源 IP 的功能(参考这里)封装成一个中介程序,让 ASP.NET Core 应用程式,能轻松地加上新功能。


middleware 读取 request body 的情境

  • 透过 await reader.ReadToEndAsync() 读取出来
  • 再将 httpContext.Request.Body.Position 归 0 即可 !
/// <summary>
/// 判断 SqlInjection - 不判断档案上传
/// </summary>
public class SqlInjectionMiddleware
{
    public SqlInjectionMiddleware(RequestDelegate                   next,
                                    SqlInjectionValidateStringService sqlInjectionValidateStringService)
    {
        _next                              = next;
        _sqlInjectionValidateStringService = sqlInjectionValidateStringService;
    }
    private readonly RequestDelegate                   _next;
    private readonly SqlInjectionValidateStringService _sqlInjectionValidateStringService;
    private readonly HashSet<string> _validateRequestBodyHttpMethods = new[] { "Post", "Patch", "Put" }.ToHashSet();
    public async Task Invoke(HttpContext context)
    {
        ValidateQueryString(context);
        if (_validateRequestBodyHttpMethods.Contains(context.Request.Method, StringComparer.CurrentCultureIgnoreCase)
            && string.IsNullOrWhiteSpace(context.Request.ContentType)      == false
            && context.Request.ContentType.Contains("multipart/form-data") == false)
        {
            await ValidateRequestBodyAsync(context);
        }
        await _next(context);
    }
    /// <summary>
    /// 检查 Query String
    /// </summary>
    private void ValidateQueryString(HttpContext context)
    {
        var queryString = context.Request.QueryString.ToString();
        _sqlInjectionValidateStringService.Validate(queryString);
    }
    /// <summary>
    /// 检查 Request Body
    /// </summary>
    private async Task ValidateRequestBodyAsync(HttpContext context)
    {
        context.Request.EnableBuffering();
        var body = await GetRawBodyStringAsync(context, Encoding.UTF8);
        _sqlInjectionValidateStringService.Validate(body);
    }
    private async Task<string> GetRawBodyStringAsync(HttpContext httpContext, Encoding encoding)
    {
        var body = String.Empty;
        if (httpContext.Request.ContentLength       == null
            || (httpContext.Request.ContentLength > 0) == false
            || httpContext.Request.Body.CanSeek        == false)
        {
            return body;
        }
        httpContext.Request.Body.Seek(0, SeekOrigin.Begin);
        using (var reader = new StreamReader(httpContext.Request.Body,
                                                encoding,
                                                true,
                                                1024,
                                                true))
        {
            body = await reader.ReadToEndAsync();
        }
        httpContext.Request.Body.Position = 0;
        return body;
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值