0

我正面临一个奇怪的错误,其中 .NET Core 2.1 API 在某些情况下似乎忽略了 JSON 主体。

  • 我建议了许多其他问题(例如this one,它本身引用了其他问题),但无法解决我的问题。

我有类似以下 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 转换器的CanConvert, CanRead, ReadJson,然后路由到SendInvoice包含body解析的 json 的路径。

但是,使用HttpWebRequest调用 api 方法(从 .NET Framework 4,如果这很重要)只会通过CanConvert,然后路由到SendInvoicenull body

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

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

当我直接阅读正文时,我得到了两种情况的预期值:

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

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

在此处输入图像描述

可以肯定的是,Content-Type标题设置为application/json. 此外,FWIW,HttpWebRequest正文设置如下:

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

并调用:

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

问题

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

4

1 回答 1

0

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

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 字符:

它返回一个提供 Unicode 字节顺序标记 (BOM) 的 UTF8Encoding 对象。要实例化不提供 BOM 的 UTF8 编码,请调用 UTF8Encoding 构造函数的任何重载。

事实证明,按照规范,不允许在 json 中传递 BOM :

实现不得在网络传输的 JSON 文本的开头添加字节顺序标记 (U+FEFF)。

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

最后,至于为什么以下方法起作用请参见此处的演示):

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

我不太确定。当然,默认情况下StreamReader也使用UTF8属性(请参见此处的备注),因此它不应该删除 BOM,实际上它没有。根据我所做的测试(在此处查看),它似乎ReadToEnd负责删除 BOM。

详细说明:

于 2021-12-12T10:35:30.707 回答