你现在多半卡在一个很别扭的场景里:代码明明按 JSON 去反序列化,报错却从“基”这个汉字开始;看起来像格式错了,往下看原始内容,又发现是“基础连接已经关闭: 服务器关闭了本应保持活动状态的连接”。这时候很容易走偏,有人盯着 JSON 模型改字段名,有人去怀疑编码,有人重试几十次。更常见的情况是,接口根本没有返回你以为的 JSON,而是返回了一段异常文本,或者连接在响应完成前就断了。
先确认一件事:你拿到的到底是不是接口正文
报错里的 Unexpected character encountered while parsing value: 基 已经给了一个很直接的信号:反序列化器读到的第一个有效字符不是 {、[、引号,也不是数字,而是汉字“基”。这几乎可以排除“某个字段类型不匹配”这一类问题,因为类型错误通常发生在 JSON 已经成功读入之后,不会卡在 position 0。
如果 position 0 就失败,排查方向应该立刻转向“响应内容与预期不一致”。最实用的动作不是继续看异常堆栈,而是把 HTTP 状态码、响应头和响应体原文完整打印出来。很多项目里只记录了异常 message,没有保留原始响应,结果开发者看到“解析失败”就去改 DTO,改了半天没有一点变化。
假设你抓到的响应体第一行就是“基础连接已经关闭……”,那它已经不是 JSON 解析问题本身,而是上游连接、代理、网关或服务端在别的环节出了错,只是最终以纯文本形式被你拿到了。
“基础连接已经关闭”通常出现在什么位置
这句话经常来自 .NET、某些旧版 HTTP 客户端、反向代理后的错误包装,或者服务端把内部异常直接写回了响应流。它不一定说明你的业务接口逻辑有 bug,很多时候问题发生在业务代码跑到一半之前。
有几种场景最常见。
一是长连接被提前回收。客户端还以为连接可复用,服务端、负载均衡器或中间代理已经把它关了。下一次请求复用这个连接时,连接刚建立好又被对方断开,于是客户端收到一段异常信息,随后你的代码又把这段文本当 JSON 去读。
二是请求耗时过长。服务端处理慢,代理等待超时,连接被切断。你看到的可能不是 500 页面,而是一段框架生成的文本说明。这个情况在导出、大查询、上传、跨服务链路调用时比较多。
三是 HTTPS、TLS 或证书协商有问题。协商失败后,有的组件会抛网络层异常,有的组件会包装成通用连接关闭信息。如果外层又统一按字符串返回,你最后看到的仍然是解析失败。
四是服务端返回了错误页。比如网关限流、鉴权失败、WAF 拦截、Nginx 502、CDN 回源异常。页面可能是 HTML,也可能是一段中文提示。只要客户端没有先判断 Content-Type,就会把这些内容塞进 JSON 解析器。
不要一上来改模型,按这个顺序看四个点
- 看状态码。 如果不是 200 段,别直接反序列化。404、500、502、503、504 都可能返回文本或 HTML。很多“解析失败”其实是错误处理流程缺失。
- 看 Content-Type。 预期应该是 application/json。如果拿到的是 text/plain、text/html,甚至没有这个头,说明你已经偏离了正常响应路径。
- 看响应体开头几十个字符。 开头是“基”“<html”“Server Error”“Bad Gateway”,定位速度会快很多。position 0 的字符往往最有价值。
- 看连接复用与超时设置。 如果同一个接口偶发报错、重试又好,优先怀疑 Keep-Alive、代理超时、连接池复用、空闲连接过期,而不是业务 JSON 格式随机变化。
什么时候该查客户端,什么时候该去找服务端或网关
如果每次请求都在同一个接口、同一个环境稳定报错,而且抓包显示服务端从头到尾都只返回那段中文文本,你就该把注意力放到服务端日志、网关日志、上游依赖异常上。尤其是请求压根没有进入控制器,或进入后没有业务日志,通常问题在控制器之前。
如果只有个别机器、个别时间段出现,重试后恢复,或者同一份代码在本地正常、在线上偶发失败,客户端和网络层更值得看。连接池是否共用过度、DNS 是否切换、代理是否回收空闲连接、请求超时是否比服务端更短,这些都会制造“偶发解析失败”。
再看频率。如果每次都是大响应体失败,小响应体正常,方向就偏向响应传输中断、压缩解压异常、代理缓冲区限制。要是只有登录后接口出错,匿名接口正常,则应检查鉴权过期、跳转到登录页、单点登录中间页返回 HTML。
代码上该补的不是“多 try-catch”,而是两层保护
很多项目在这里的处理方式很单薄:请求发出去,拿到字符串,直接 DeserializeObject。这会把网络层异常、网关错误页、业务 JSON 错误全都混成一种“解析失败”。你后面看到的堆栈会越来越没用。
更稳妥的做法是把判断拆开。第一层判断 HTTP 是否成功,以及返回类型是否符合预期;第二层才做 JSON 反序列化。遇到异常时,把状态码、Content-Type、响应体前几百个字符、请求地址、请求方法、耗时一起记日志。这样你下次再看到“❌ 响应解析失败(非 JSON 或损坏):Unexpected character encountered while parsing value: 基. Path '', line 0, position 0. 原始内容:基础连接已经关闭: 服务器关闭了本应保持活动状态的连接。”,就不用靠猜。
如果你正是在某个名为这段报错信息的封装工具、插件或内部页面里看到提示,建议立刻给它补一个功能:在解析失败时显示原始响应头和响应体预览,而不是只抛出解析异常。这个改动很小,但能把排查时间从半天压到几分钟。
还有一个常被忽略的点:别把服务端异常原文直接透给前端或调用方。开发阶段这么做方便,线上环境里会混淆问题边界,还可能暴露内部组件信息。更合适的做法是服务端返回统一错误结构,日志里保留完整异常详情。
如果你现在手里只有这一条异常,下一步不要改 JSON 字段,也不要重装库,去抓一次完整响应,确认状态码、Content-Type 和响应体第一行是什么。
你现在多半卡在一个很别扭的场景里:代码明明按 JSON 去反序列化,报错却从“基”这个汉字开始;看起来像格式错了,往下看原始内容,又发现是“基础连接已经关闭: 服务器关闭了本应保持活动状态的连接”。这时候很容易走偏,有人盯着 JSON 模型改字段名,有人去怀疑编码,有人重试几十次。更常见的情况是,接口根本没有返回你以为的 JSON,而是返回了一段异常文本,或者连接在响应完成前就断了。
先确认一件事:你拿到的到底是不是接口正文
报错里的 Unexpected character encountered while parsing value: 基 已经给了一个很直接的信号:反序列化器读到的第一个有效字符不是 {、[、引号,也不是数字,而是汉字“基”。这几乎可以排除“某个字段类型不匹配”这一类问题,因为类型错误通常发生在 JSON 已经成功读入之后,不会卡在 position 0。
如果 position 0 就失败,排查方向应该立刻转向“响应内容与预期不一致”。最实用的动作不是继续看异常堆栈,而是把 HTTP 状态码、响应头和响应体原文完整打印出来。很多项目里只记录了异常 message,没有保留原始响应,结果开发者看到“解析失败”就去改 DTO,改了半天没有一点变化。
假设你抓到的响应体第一行就是“基础连接已经关闭……”,那它已经不是 JSON 解析问题本身,而是上游连接、代理、网关或服务端在别的环节出了错,只是最终以纯文本形式被你拿到了。
“基础连接已经关闭”通常出现在什么位置
这句话经常来自 .NET、某些旧版 HTTP 客户端、反向代理后的错误包装,或者服务端把内部异常直接写回了响应流。它不一定说明你的业务接口逻辑有 bug,很多时候问题发生在业务代码跑到一半之前。
有几种场景最常见。
一是长连接被提前回收。客户端还以为连接可复用,服务端、负载均衡器或中间代理已经把它关了。下一次请求复用这个连接时,连接刚建立好又被对方断开,于是客户端收到一段异常信息,随后你的代码又把这段文本当 JSON 去读。
二是请求耗时过长。服务端处理慢,代理等待超时,连接被切断。你看到的可能不是 500 页面,而是一段框架生成的文本说明。这个情况在导出、大查询、上传、跨服务链路调用时比较多。
三是 HTTPS、TLS 或证书协商有问题。协商失败后,有的组件会抛网络层异常,有的组件会包装成通用连接关闭信息。如果外层又统一按字符串返回,你最后看到的仍然是解析失败。
四是服务端返回了错误页。比如网关限流、鉴权失败、WAF 拦截、Nginx 502、CDN 回源异常。页面可能是 HTML,也可能是一段中文提示。只要客户端没有先判断 Content-Type,就会把这些内容塞进 JSON 解析器。
不要一上来改模型,按这个顺序看四个点
- 看状态码。 如果不是 200 段,别直接反序列化。404、500、502、503、504 都可能返回文本或 HTML。很多“解析失败”其实是错误处理流程缺失。
- 看 Content-Type。 预期应该是 application/json。如果拿到的是 text/plain、text/html,甚至没有这个头,说明你已经偏离了正常响应路径。
- 看响应体开头几十个字符。 开头是“基”“<html”“Server Error”“Bad Gateway”,定位速度会快很多。position 0 的字符往往最有价值。
- 看连接复用与超时设置。 如果同一个接口偶发报错、重试又好,优先怀疑 Keep-Alive、代理超时、连接池复用、空闲连接过期,而不是业务 JSON 格式随机变化。
什么时候该查客户端,什么时候该去找服务端或网关
如果每次请求都在同一个接口、同一个环境稳定报错,而且抓包显示服务端从头到尾都只返回那段中文文本,你就该把注意力放到服务端日志、网关日志、上游依赖异常上。尤其是请求压根没有进入控制器,或进入后没有业务日志,通常问题在控制器之前。
如果只有个别机器、个别时间段出现,重试后恢复,或者同一份代码在本地正常、在线上偶发失败,客户端和网络层更值得看。连接池是否共用过度、DNS 是否切换、代理是否回收空闲连接、请求超时是否比服务端更短,这些都会制造“偶发解析失败”。
再看频率。如果每次都是大响应体失败,小响应体正常,方向就偏向响应传输中断、压缩解压异常、代理缓冲区限制。要是只有登录后接口出错,匿名接口正常,则应检查鉴权过期、跳转到登录页、单点登录中间页返回 HTML。
代码上该补的不是“多 try-catch”,而是两层保护
很多项目在这里的处理方式很单薄:请求发出去,拿到字符串,直接 DeserializeObject。这会把网络层异常、网关错误页、业务 JSON 错误全都混成一种“解析失败”。你后面看到的堆栈会越来越没用。
更稳妥的做法是把判断拆开。第一层判断 HTTP 是否成功,以及返回类型是否符合预期;第二层才做 JSON 反序列化。遇到异常时,把状态码、Content-Type、响应体前几百个字符、请求地址、请求方法、耗时一起记日志。这样你下次再看到“❌ 响应解析失败(非 JSON 或损坏):Unexpected character encountered while parsing value: 基. Path '', line 0, position 0. 原始内容:基础连接已经关闭: 服务器关闭了本应保持活动状态的连接。”,就不用靠猜。
如果你正是在某个名为这段报错信息的封装工具、插件或内部页面里看到提示,建议立刻给它补一个功能:在解析失败时显示原始响应头和响应体预览,而不是只抛出解析异常。这个改动很小,但能把排查时间从半天压到几分钟。
还有一个常被忽略的点:别把服务端异常原文直接透给前端或调用方。开发阶段这么做方便,线上环境里会混淆问题边界,还可能暴露内部组件信息。更合适的做法是服务端返回统一错误结构,日志里保留完整异常详情。
如果你现在手里只有这一条异常,下一步不要改 JSON 字段,也不要重装库,去抓一次完整响应,确认状态码、Content-Type 和响应体第一行是什么。