news 2026/10/15 8:04:47

对端不改格式怎么办?ASP.NET Core 接口 415 根治实录:JSON 信封 + Content-Type 不可靠的适配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
对端不改格式怎么办?ASP.NET Core 接口 415 根治实录:JSON 信封 + Content-Type 不可靠的适配

一、背景

最近做 CRM 对外 WebApi 与客服热线平台(400 呼叫中心)的接口对接:平台方要调我们的六个查询接口(部门、员工、经销商、联系人、客户、序列号)。上线联调当天,对面怎么调都是 415 Unsupported Media Type,日志里一片 ProblemDetails。

这不是第一次了——之前两个建单接口也栽过同一个坑,当时是打补丁式修的。这次决定根治。

对端平台的特征,也是这次问题的根源:

  • 请求体是JSON 信封:{"action":"queryDevice","data":{...}},真正的业务参数在data节点里;

  • Content-Type 不保证是 application/json,经常是空的或别的值;

  • 对端有自己的发布节奏,不会为了我们改调用格式——只能我们适配。

二、415 的根因:内容协商发生在 Action 之前

原来的接口是这么写的(反面教材):

[HttpPost("device-query")] [Consumes("application/json")] public IActionResult DeviceQuery([FromBody] QueryRequest request) { ... }

两个特性叠加,必然出事:

  1. [Consumes]声明"我只接受 application/json"。ASP.NET Core 按 Content-Type 选择输入格式化器(InputFormatter),这一步发生在进入 Action 之前。Content-Type 不是 application/json 时,框架直接拒绝返回 415,Action 一行代码都不会执行;

  2. [FromBody]依赖模型绑定,同样按 Content-Type 选 formatter。就算恰好带对了 Content-Type,body 是信封结构,和 DTO 对不上,绑定出来的参数要么是空对象要么直接报错。

排查时日志里的两个信号,看到就能秒定位:

  • 415 ProblemDetails+ traceId:内容协商在中间件层就拒了,请求体根本进不了业务日志;

  • 请求参数显示[Body too large]:ContentLength 为 null 时,日志中间件采集不到 body,这是它显示的占位文案。

三、根治方案:绕过内容协商,手工解析

思路:Action 不声明任何参数、不加 Consumes,把 HttpContext 拿进来自己拆包。

[HttpGet("device-query")] [HttpPost("device-query")] public Task<IActionResult> DeviceQuery() { return RunCallCenter400Async(async () => { var request = await CallCenterRequestParser.ReadDataAsync<QueryRequest>(HttpContext.Request); var data = _queryService.Query(request); return OutContractJson(1, "查询成功", data); }); }

注意GET + POST 双收:对端用哪种 HTTP 方法不稳定,两个都挂上最省心。

解析器从三个来源依次找 data,优先级如下:

public static async Task<T> ReadDataAsync<T>(HttpRequest request) { string raw; // ① 表单渠道优先:老接口历史上收 multipart/form 的 data 字段 if (request.HasFormContentType) { raw = (await request.ReadFormAsync())["data"]; } else { // ② 读原始 body 原文:不管 Content-Type 是什么都读 request.EnableBuffering(); using var reader = new StreamReader(request.Body); raw = await reader.ReadToEndAsync(); request.Body.Position = 0; } // ③ 查询串兜底:?data=... if (string.IsNullOrWhiteSpace(raw)) raw = request.Query["data"]; ​ // 信封拆解:有 data 节点取 data(字符串形态再解一层);没有 data 节点,根对象即业务参数 var doc = JsonDocument.Parse(raw); var root = doc.RootElement; var payload = root.TryGetProperty("data", out var dataProp) ? ParseDataNode(dataProp) : root.Clone(); ​ // 反序列化 + DTO 数据注解校验 var dto = payload.Deserialize<T>(jsonOptions); // JsonSerializerDefaults.Web:camelCase 不敏感 Validator.ValidateObject(dto, new ValidationContext(dto), validateAllProperties: true); return dto; }

几个要点:

  • data 节点三种形态都要兼容:对端实现不稳定——有时 data 是对象,有时是 JSON 字符串(需要再解一层),有时干脆没有 data、根对象就是业务参数。三个分支都要覆盖;

  • 反序列化用JsonSerializerDefaults.Web:camelCase 不敏感、数字字符串兼容,能吃掉对端大部分大小写和类型不齐的问题;

  • 保留 DTO 数据注解校验:绕过了框架的模型绑定,但Validator.ValidateObject要自己调用,别把参数合法性也绕没了;

  • EnableBuffering()+Body.Position = 0:想在一个请求里先读 body 再干别的(比如日志中间件还要采集 body),必须让流可重读。

四、异常收口:永远回 200,把结果放业务码

对端平台不认 HTTP 状态码,只认响应体里的业务码(它的契约是 code=1 成功 / code=0 失败)。所以哪怕解析失败,也要回 HTTP 200,把失败原因写进业务码:

private async Task<IActionResult> RunCallCenter400Async(Func<Task<IActionResult>> action) { try { return await action(); } catch (InvalidOperationException ex) // 对端发错了:解析/校验失败 → 记 Warning { _logger.LogWarning(ex, "请求格式校验失败"); return OutContractJson(0, ex.Message, null); } catch (OperationCanceledException) when (HttpContext.RequestAborted.IsCancellationRequested) { throw; } catch (Exception ex) // 我们错了 → 记 Error { _logger.LogError(ex, "接口处理失败"); return OutContractJson(0, ex.Message, null); } }

这里的设计点是:用异常类型当"信号通道"。解析器只负责抛InvalidOperationException,收口方法按异常类型分诊——它代表"对端发错了",记 Warning;其他异常代表"我们错了",记 Error。比到处 try-catch 干净,排查日志时一眼分清责任方。

五、验证:穷举请求形态

修复后本地把请求形态穷举了一遍:GET/POST × {JSON 信封、表单、平铺、查询串、空信封、非法 JSON},全部返回 200 + 业务码,不再出现 415/405。上线后对端直接可用。

六、总结

  1. [Consumes]是"声明我能吃什么",对端给什么它说了不算——对端不可控时,这层声明就是在埋雷;

  2. 绕过内容协商不是放弃安全:DTO 校验、鉴权、限流一层都不能少,只是把"拆包裹"的主动权拿回来;

  3. 接口联调永远以对端实测为准,文档只能当参考——这是血泪教训。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 8:03:35

让 CSS 渐变光斑随音量动态呼吸:基于 Web Audio 频谱的色彩平滑过渡引擎

很多音乐播放器或音乐互动专题页在设计“音频可视化光效”时&#xff0c;常常陷入两种极端&#xff1a;一种是直接使用一段预先渲染好的循环视频或 CSS 关键帧动画&#xff0c;不管音乐播到静谧的前奏还是躁烈的副歌&#xff0c;背景永远按照固定的频率机械闪烁&#xff1b;另一…

作者头像 李华
网站建设 2026/10/15 8:03:26

盲盒小程序开发:技术架构与高并发优化实战

1. 盲盒经济背后的技术需求拆解这两年线下商场里突然冒出来的盲盒自动贩卖机&#xff0c;相信大家都不陌生。这种"花小钱买未知惊喜"的玩法&#xff0c;正在从潮玩圈蔓延到美妆、文具甚至生鲜领域。作为开发者&#xff0c;去年我接手了三个不同行业的盲盒小程序项目&…

作者头像 李华
网站建设 2026/10/15 8:03:25

物理AI控制器新思路:HarnessPAI进化式策略搜索框架实践

干物理AI这一行的都知道&#xff0c;最大的麻烦往往不是算法不够聪明&#xff0c;而是物理世界太“不讲道理”。我最早用强化学习做四足机器人步态&#xff0c;调了一周奖励函数&#xff0c;仿真里跑得飞起&#xff0c;一到真机上就开始原地打转&#xff0c;后来才意识到&#…

作者头像 李华
网站建设 2026/10/15 8:03:13

AI 产品经理需要什么技能?

「AI 产品经理需要什么技能&#xff1f;」 这是被问得最多的问题&#xff0c;但多数人心里早已有答案——只是那个答案是错的。 先把三个最常见的错答摆出来。 错答一&#xff1a;要会写代码、会训模型。这是把 AI 产品经理当成了算法工程师的入门版。你不需要训练模型&…

作者头像 李华
网站建设 2026/10/15 8:03:06

STP生成树协议原理解析:防止广播风暴与环路的二层守护机制

1. STP生成树到底在解决什么问题&#xff1f;——一个被低估的“网络交通协管员”你有没有遇到过这样的场景&#xff1a;某天下午三点&#xff0c;公司办公网突然大面积掉线&#xff0c;打印机连不上、共享文件夹打不开、视频会议卡成幻灯片&#xff0c;但路由器指示灯明明全亮…

作者头像 李华
网站建设 2026/10/15 8:02:57

AMD GPU跑通蚂蚁灵波具身大模型:ROCm+PyTorch实战指南

1. 这不是“跑通就行”的比赛&#xff0c;而是一场硬件适配能力的实战检验你手头有一张AMD Radeon RX 7900 XTX&#xff0c;显存24GB&#xff0c;PCIe 5.0带宽拉满&#xff0c;但打开蚂蚁灵波具身大模型挑战赛官网&#xff0c;第一行就写着&#xff1a;“推荐环境&#xff1a;N…

作者头像 李华