踩坑WebService接入外部非法数据(非打印字符)。

由于hugo渲染的时候会检查标题title内的非打印字符,所以这里把带有非打印字符的标题写到正文中。 标题:晋西 != 晋西 ??? hugo渲染报错:Error: Error building site: “/Users/dragonsong/Downloads/code-repo/dragonsong031100/content/posts/rd/troubleshoot/invalid_nonprinting_char.md:2:1”: failed to unmarshal YAML: yaml: control characters are not allowed

背景

19年开始我作为一个数据同步服务的owner开始整合各种外部数据接入的架构落地、编码设计。其中有一个客户提供老式的WebService协议的接口,我开发了一个针对该协议的agent客户端,负责把数据拉过来、同步(处理后置逻辑)。

听起来很简单的过程:调用外部接口,拿到数据就可以扔给后面的服务。

我们在前置的dev、sit环境都经过了测试,完全没有问题,接下来就往prod环境推。

一推,就出问题了。

使用了cxf框架处理协议的逻辑,框架内报了下面这个错误:

:[failed to localize] Failed to deserialize the response.(javax.xml.bind.UnmarshalException
- with linked exception:
[com.ctc.wstx.exc.WstxUnexpectedCharException: Illegal character ((CTRL-CHAR, code 8))
at [row,col {unknown-source}]: [1,169325]])

定位

首先我当时考虑,一样的程序(检查下仓库分支、发布在不同环境使用的hash码)在前置环境未出问题,那么不同的环境下有哪些东西是不一样的呢?(这种情况肯定是变化的内容导致了问题)

  1. 机器环境不同 但是我们这个程序只是作为客户端,机器环境只负责将应用跑起来,并不会影响这里拉数据的运行时状态
  2. 数据不同 这点取决于外部,分析下来也只有这里有问题

其次,顺着异常栈,找一下cxf中抛异常的逻辑。

cxf框架中拿到字节后反序列化时针对非打印字符(ctrl-char)的报错异常栈

而其源码中的抛错逻辑很明确:碰到ctrl-char就直接抛出异常。目的是排除这类非法数据,在编写本文时,hugo中也有类似逻辑(不允许我直接在标题中使用非打印字符)。

当时我定位问题的时候走了一些弯路,标题中的「晋西 != 晋西 ???」是数据中的字符,而我当时完全忘记了有非打印字符这回事,我尝试把接口拿到的数据在本地打开,使用ctrl+f匹配字符时才发现,明明我两只眼睛都看到了晋西这两个字,但是就是匹配不上。明白原因之后才恍然大悟,中间有一个\b非打印字符,所以开始用眼睛来查问题,查错了方向。

此次之后,我们一定要铭记非打印字符的存在,这些字符用眼睛是看不到的,切忌用错误的方式来排查问题。

Java中使用Character.isISOControl(char)进行判断。

关于ctrl-char的范围、概念可以参考这里:List of Unicode Characters of Category “Control”

解决

web相关的框架都提供了拦截器机制,我们利用cxf提供的这类机制自定义一个FilterCtrlCharInterceptor,内部方法将此类非法字符排除。

其中排除的逻辑可以用正则直接匹配替换:

1
string.replaceAll("([\b|\f|\n|\r|\t])", "")

也可以直接使用guava库:

1
2
3
CharMatcher.JAVA_ISO_CONTROL.removeFrom(string);

CharMatcher.javaIsoControl()

小结

本文通过经验判断、异常栈分析的方式排查了cxf框架中数据反序列化的异常检查问题。解决方式非常简单,直接正则匹配替换非法字符即可。

同时,我们可以控制的数据范围内,一定要有主动排除非法字符的逻辑,防止这类数据写库。