背景
收到隔壁组请求支援,定位调用接口报错,报错日志如下:
2026-08-28 16:07:22.462 [http-nio-8003-exec-14] ERROR [UnknownExceptionHandler:28]com.google.gson.JsonSyntaxException on: java.io.EOFException: End of input at line 1 column 1479 path $.notification[5].lastUpdateDate
at com.google.gson.Gson.fromJson(Gson.java:1370)
at com.google.gson.Gson.fromJson(Gson.java:1262)
at com.google.gson.Gson.fromJson(Gson.java:1171)
at com.google.gson.Gson.fromJson(Gson.java:1107)
…
从报错日志初步来看,像是json不完整,解析过程中,碰到结束符了。
他们提供了代码,从代码逻辑看非常简单,他们先通过RestTemplate转成String,然后在手工序列化。报错就发生在序列化那一行。
Stringresponse=restTemplate.doGet(url,headers,String.class);TkbHotResponsetkbHotResponse=GSON.fromJson(response,TkbHotResponse.class);系统组网如下:
定位思路
先定界-是不是json不完整
从报错来看,像是json不完整,首先得把这个方向确认了,才好继续往下定位,因此通过堆栈,稍微看了下Gson的源码,确认后着手定位为什么输入的字符串不是完整的json。
初步想法是不是上游因为啥原因导致传输的json就不完整。隔壁组提供了使用postman调用的响应,里面内容是完整的。该场景排除。
为什么restTemplate返回的String不是完整的json
通过上一步的分析,上游传的数据是没问题的,那初步认为是不是偶发网络提前中断了,导致数据没传输完?
如果是上述场景的话,那按说RestTemplate会抛异常吧?看了前后日志并没有异常,难道RestTemplate接受到200的响应,如果读取响应过程中连接中断,不会有异常?遂初步翻翻RestTemplate的源码,看看反序列化(网络字节流转String)的过程是怎么样的。
protectedStringreadInternal(Class<?extendsString>clazz,HttpInputMessageinputMessage)throwsIOException{Charsetcharset=getContentTypeCharset(inputMessage.getHeaders().getContentType());longlength=inputMessage.getHeaders().getContentLength();byte[]bytes=(length>=0&&length<=Integer.MAX_VALUE?inputMessage.getBody().readNBytes((int)length):inputMessage.getBody().readAllBytes());returnnewString(bytes,charset);}通过阅读StringHttpMessageConverter代码发现以下可疑点:
如果传来"Content-Length"头,会根据这个头的长度来读取响应。
我感觉像是传了错误的"Content-Length"头,因为如果是网络问题,报错概率应该很小,但是我看日志还挺多的。
为此我找他们用postman调用了一下,看看是不是有传这个头。结果还真有
从他们上面截图可以看到,除了传"Content-Length"头,还传了"Content-Encoding:gzip"。
我感觉应该是启用了压缩,然后"Content-Length"头传的是压缩后的值。如果像猜想一样的话,那这个问题应该是必现,找隔壁项目组求证后,得到了肯定的答复。为此坚定了这个定位方向。接下来就是找上游确认"Content-Length"是怎么写进去的,压缩到底是谁做的。
和上游定位Content-Length是怎么传的里面的值怎么来的
先是找他们确认"Content-Length"头是他们传的还是ALB传的,通过访问了单边地址(防止ALB的干扰);发现这个头确实是他们自己传的。
然后就联合上游,分析他们代码咋写的这头?是他们自己代码写的还是tomcat写的,通过分析代码发现,确实是他们自己写的这个头。
publicvoiddoWrite(byte[]content,HttpServletResponsehttpResponse)throwsIOException{httpResponse.setContentType("text/html; charset=UTF-8");httpResponse.addHeader("Content-Length",Integer.toString(content.length));httpResponse.addHeader("Content-Encoding","gzip");StreamUtil.transfer(newBufferedInputStream(newByteArrayInputStream(content)),httpResponse.getOutputStream());}# doWrite中的content参数,从pullHtml函数中获取publicbyte[]pullHtml(HttpServletRequestrequest,HttpServletResponseresponse,WebRequestVOwebRequest,XhtmBeanxhtmBean)throwsIOException{returntoGzipByteArray(createHtml(request,response,webRequest,xhtmBean),UTF_8);}从他们代码中得到一下信息,他们会自己对数据做压缩,压缩完后,同时传了Content-Length和gzip的压缩头。所以"Content-Length"头的值是没问题的,代表的是HTTP响应中,body的真实字节数。
至此,问题根因初步明确了,RestTemplate拿到的Content-Length的值是压缩后的,导致反序列化时字节数读少了。
接下来的问题就是为什么测试环境没这个问题,而生产有这个问题。
生产和测试的差异
通过对比生产和测试环境的响应,发现响应头有些差异:测试环境没有Content-Length头,变成了"Transfer-Encoding:chunked"
| 生产环境 | 测试环境 |
|---|---|
| Content-Length: 1542 | Transfer-Encoding:chunked |
| Content-Encoding:gzip | Content-Encoding:gzip |
从结果来看像是生产环境ALB完全透传后台的数据,而测试环境像是先解压然后重新压缩,这样有"Transfer-Encoding:chunked"。
为了确认是ALB的问题,我们绕过ALB调用了单边地址(直接调用微服务),发现测试/生产都一样,返回的头与代码逻辑一致(包含"Content-Length"和"Content-Encoding:gzip")。以上得出结论:相同响应,经过不同环境的ALB后产生了差异。
ALB其实也就是ningx,在找他们之前自己也做了功课,搜了一下"nginx什么情况下会解压后端数据后,重新压缩",AI告知:
场景一:Nginx 开启了内容替换或修改模块(最常见)如果你在 Nginx 中配置了对网页内容的实时修改(例如修改域名、注入脚本、替换文本),Nginx 必须拿到明文内容才能进行字符串匹配和替换。
我们并看不到ALB的配置,因此找他们求证,得到的答复是,我们测试环境的网页上面会显示"BETA"的样式,这个是他们拦截了请求,并注入了特定的内容,而生产没有,到此疑问得以消除,AI的答案还是很有参考性的。
目前还剩最后一个疑问,为什么之前没有,而现在才有,双方都检查一下包,上游没有任何修改,而隔壁组,最近升级了开源软件。HttpClient5由5.5升级到5.6.x了,通过调试发现:在内容压缩的情况下,两者对"Content-Length"头的处理由差异,前者会吞掉它,而后者原封不动的给调用方,因此RestTemplate拿到了错误的值。
此致所有疑问得以解决。
小插曲
实际上前面,我并不知道上游应用自己做了压缩(没看到对应代码),为此我还看了tomcat的源码,看应用层传"Content-Encoding:gzip"头,tomcat会做何处理,tomcat何种情况下会进行压缩,压缩后会不会覆盖应用层写的"Content-Length"。
毕竟初步看他们代码时,没看仔细,没发现他们自己有做压缩。既然他们应用层传的值没问题,那很有可能是tomcat进行了压缩,并覆盖了该值。
通过源码确认以下事实:
tomcat靠全局配置启用压缩,应用层传了"Content-Encoding:gzip"头后,tomcat认为应用层已经压缩过了,就算tomcat启用了压缩,也不会再次做压缩。
经此分析后,出现了矛盾的地方,应用层传了正确的值,tomcat又不做压缩,那为什么通过访问单边地址,看到的"Content-Length"头的值不正确?为此和他们一起仔细调试了代码,然后就发现他们自己代码做了压缩,该值是压缩后的值。