ASP.NET Core 2.1 API POST body 使用 HttpWebRequest 调用时为 null,似乎无法解析为 JSON

ASP.NET Core 2.1 API POST body is null when called using HttpWebRequest, seems it can't be parsed as JSON

我遇到了一个奇怪的错误,其中 .NET Core 2.1 API 在某些情况下似乎忽略了 JSON body。

我有类似下面的 API 方法:

[Route("api/v1/accounting")]
public class AccountingController
{                                            sometimes it's null
                                                       ||
    [HttpPost("invoice/{invoiceId}/send")]             ||
    public async Task<int?> SendInvoice(               \/
         [FromRoute] int invoiceId, [FromBody] JObject body
    ) 
    {
        // ...                                                                   
    }
}

相关配置为:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
     services
       .AddMvcCore()
       .AddJsonOptions(options =>
        {
           options.SerializerSettings.Converters.Add(new TestJsonConverter());
        })
       .AddJsonFormatters()
       .AddApiExplorer();
     
     // ...
}

其中 TestJsonConverter 是我创建的一个简单转换器,用于测试为什么事情不能正常工作,就这么简单:

public class TestJsonConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var token = JToken.Load(reader);
        return token;
    }
    public override bool CanRead
    {
        get { return true; }
    }
    public override bool CanConvert(Type objectType)
    {
        return true;
    }
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException("Unnecessary (would be neccesary if used for serialization)");
    }
}

使用 Postman 调用 api 方法有效,这意味着它通过 JSON 转换器的 CanConvertCanReadReadJson,然后路由到SendInvoicebody 包含已解析的 json.

但是,使用HttpWebRequest调用api方法(来自.NET Framework 4,如果重要的话)只会通过 CanConvert,然后路由到 SendInvoicebody 为空。

请求 body 只是一个简单的 json,类似于:

{
    "customerId": 1234,
    "externalId": 5678
}

当我直接读取 body 时,我得到了两种情况下的期望值:

using (var reader = new StreamReader(context.Request.Body))
{
   var requestBody = await reader.ReadToEndAsync(); // works
   var parsed = JObject.Parse(requestBody);
}

我看不出这两种请求之间有任何有意义的区别 - 左边是 Postman 的请求,右边是 HttpWebRequest:

可以肯定的是,Content-Type header 设置为 application/json。此外,FWIW,HttpWebRequest body 设置如下:

using(var requestStream = httpWebRequest.GetRequestStream())
{
    JsonSerializer.Serialize(payload, requestStream);
}

并调用:

var response = (HttpWebResponse)request.GetResponse();   

问题

为什么 bodyHttpWebRequest 一起使用时为空?为什么在这种情况下会跳过 JSON 转换器读取方法?

问题出在序列化的底层代码中。所以这一行:

JsonSerializer.Serialize(payload, requestStream);

使用默认 UTF8 属性:

实现
public void Serialize<T>(T instance, Stream stream)
{
   using(var streamWriter = new StreamWriter(stream, Encoding.UTF8) // <-- Adds a BOM
   using(var jsonWriter = new JsonTextWriter(streamWriter))
   {
       jsonSerializer.Serialize(jsonWriter, instance); // Newtonsoft.Json's JsonSerializer
   } 
}

默认UTF8 属性加一个BOM字符,如documentation:

It returns a UTF8Encoding object that provides a Unicode byte order mark (BOM). To instantiate a UTF8 encoding that doesn't provide a BOM, call any overload of the UTF8Encoding constructor.

原来在json中传递BOM是不允许的per the spec:

Implementations MUST NOT add a byte order mark (U+FEFF) to the beginning of a networked-transmitted JSON text.

因此 .NET Core [FromBody] 内部反序列化失败。

最后,至于为什么以下有效(see demo here):

using (var reader = new StreamReader(context.Request.Body))
{
   var requestBody = await reader.ReadToEndAsync(); // works
   var parsed = JObject.Parse(requestBody);
}

我不太确定。当然,StreamReader也默认使用UTF8属性(see remarks here), so it shouldn't remove the BOM, and indeed it doesn't. Per a test I did (see it here),好像是ReadToEnd负责去掉BOM。

详细说明: