JMeter登录JSON解析错误:Unexpected character排查 JMeter 发登录请求接口响应里直接甩回来一句JSON parse error: Unexpected character后面还跟着一个十六进制 code 和一个位置偏移量——这个画面我见过太多次了。绝大多数人第一次遇到都会本能地去翻 JSON 语法逐字逐句对括号结果折腾一小时发现括号是配平的数据也没少。真正的问题往往藏在你看不见的地方一个复制粘贴带进来的单引号一个文件头部的 BOM一个从 CSV 里读出来的空格甚至是一个全角冒号。这篇东西就是把这几年我在做接口脚本时攒下来的排查路径、配置模板和踩过的坑从头到尾捋一遍给正在被这条报错卡住的同行一个能直接照着走的路线。1. 这条报错到底在说什么先别急着改代码1.1 报错出自服务端解析器不是 JMeter 自己先把责任边界划清楚这一步能省掉一半的瞎猜。JSON parse error: Unexpected character这句话不是 JMeter 的报错JMeter 根本不解析你的请求体它只负责把字节流按你配置的内容发出去收到响应原样展示。这句话是服务端的 JSON 反序列化组件吐出来的常见于 Java 生态里处理RequestBody的那一层框架在把请求体读成对象时发现某个位置的字符不符合 JSON 语法于是抛出异常包装成 HTTP 400 返回JMeter 只是把这个响应体显示在查看结果树里。理解这一点非常关键因为它决定了你排查的方向问题一定出在JMeter 实际发出去的字节流上而不是出在你的接口逻辑、断言配置或者线程组设置上。很多人会去检查响应断言、检查正则提取器、检查 CSV 配置方向全错了。你要做的事情只有一件——搞清楚 JMeter 到底发出去了什么然后和服务端期望的东西做对比。这个对比工作听起来简单但真正做扎实的人不多大部分人是靠猜改一版发一次用试错代替定位运气好三五次碰上运气不好一下午就没了。另外一个容易混淆的点有时候你在浏览器里手动点一下登录是通的用 Postman 发也是通的唯独 JMeter 报错。这不代表 JMeter 有 bug恰恰说明两边发的东西不一样。差异可能来自请求头、可能来自编码、可能来自 body 里的一个不可见字符。找到这个差异问题就解决了。1.2 分清 Unexpected character 和 Unexpected end-of-input这两种报错长得像但指向完全相反的原因。Unexpected character意味着解析器读到了内容但内容里某个位置的字符是非法的说明 body 有东西只是有脏字符。而Unexpected end-of-input或者Required request body is missing意味着 body 是空的或者被截断了解析器读到一半发现没了。这个区分极具实操价值。如果你看到的其实是Unexpected end-of-input那你要查的是为什么 body 没发出去——可能是 HTTP 请求采样器里切到了 Body Data 但内容为空可能是请求方法配置成了 GET 导致 body 被丢弃可能是重定向把 POST 变成了 GET也可能是变量替换后整段变成了空字符串。这时候去查非法字符是南辕北辙。而既然你现在看到的是Unexpected character就可以确定body 发出去了长度不为零里面有内容问题定位在字符层面。范围一下子缩小到一个字符串里剩下的就是把这个字符揪出来。我一般会跟人说这条报错其实是好消息它比请求超时连接被重置友好太多了因为它告诉你问题就在你手里这几十个字节里。1.3 报错里那个 code 值才是真正的金矿很多人看报错只看前半句后面括号里的内容直接略过这是最大的浪费。主流的 JSON 解析器在报错时会把非法字符本身和它的码点一起打出来格式大致是Unexpected character (x (code 120))有些还会带上行列号。这个 code 值基本等于直接把答案递到你手上。我整理了一张对照表遇到报错先来这里对一眼能省很多时间code 值对应字符典型来源34双引号引号嵌套没转义或者 JSON 里出现了裸引号39单引号从 curl 命令整段复制把外层单引号也粘进来了239ïUTF-8 BOM 的第一个字节 EF 被按单字节解读44逗号,对象或数组尾部多了逗号或者中间多了一个47斜杠/JSON 里写了//注释92反斜杠\字符串里的反斜杠没写成\\123 / 125{}大括号错位或者 body 里套了两层8220 / 8221全角引号“”从需求文档、聊天记录里复制过来的65306全角冒号手写 JSON 时输入法没切回来看到 239 就直接去查文件编码和 BOM看到 39 就去查是不是从命令行复制的看到 8220 或 65306 就去找是谁从文档里粘贴的。这一步做对了很多时候一分钟就能定位根本不用往下走完整套流程。养成看 code 值的习惯是从能修到修得快的分水岭。2. 逐条拆解把非法字符找出来的六个方向2.1 头号嫌疑从 curl 或文档复制带来的引号和全角符号这是我在实际项目里遇到频率最高的一类没有之一。场景通常是这样的开发同学在终端里调通了接口写了一条 curl 命令-d {username:admin,password:123456}然后要把这条命令搬到 JMeter 里做压测。复制粘贴的时候外面那层单引号有时候跟着一起进去了Body Data 里变成{username:admin}第一个字符就是单引号服务端解析器读到的第一个非空白字符不是{直接报Unexpected character ( (code 39))。更隐蔽的是 shell 转义留下的痕迹。有些 curl 命令为了在双引号环境里写 JSON会对内部的双引号做转义写成{\username\:\admin\}整个字符串里满是反斜杠。如果原样粘到 JMeter 的 Body Data服务端看到的是{\username\:\admin\}第一个字符是{没问题第二个字符是反斜杠解析器期待的是引号于是报Unexpected character (\ (code 92))。这种情况下人眼扫过去觉得括号引号都在啊实际上是转义层数搞错了。第二大类是从文档或聊天工具里复制 JSON 示例。产品文档、接口文档、甚至钉钉群里的消息标点符号经常被自动替换成全角。{usernameadmin}里那个冒号是全角 UFF1A人眼看几乎没区别但解析器读到码点 65306 的字符直接判定非法。全角引号同理“username”看起来就是引号实际是 U201C 和 U201D跟 ASCII 的 0x22 完全不是一个东西。我在团队里定过一条规矩JSON 请求体一律不手工敲从已验证的源文件复制且复制完必须在支持字符可视化的编辑器里过一眼。这条规矩推行之后这类报错下降了大概八成。注意Body Data 里粘贴内容后先切到纯文本编辑器把不可见字符显示出来再贴回去比在 JMeter 那个小文本框里盯半天有效得多。2.2 看不见的 BOM一个字节就能让你怀疑人生BOM 这个问题坑过太多人因为它在绝大多数编辑器里完全不可见。UTF-8 的 BOM 是三个字节EF BB BF放在文件开头。有些解析器会容忍它有些不会。当服务端用严格模式读或者按单字节流读的时候第一个字节 0xEF 会被解释成ï报错就是Unexpected character (ï (code 239))。BOM 从哪来最常见的路径是用 Windows 上某些表格软件另存为 CSV UTF-8它会自动在文件头加 BOM。然后 JMeter 的 CSV Data Set Config 把这个文件的第一行第一个字段读出来作为变量值塞进 JSON body 的最前面于是整个请求体的开头就多了三个字节。你以为 body 是{username:admin,...}实际发出去的是\xEF\xBB\xBF{username:admin,...}。还有一种情况是直接在 Body Data 里贴了从某个文件里拷贝的内容文件本身带 BOM复制的时候一起带上了。这种更难发现因为你在 JMeter 界面上看到的就是{光标放上去也看不出异常。排查手段很简单把 Body Data 的内容单独存成一个文件用十六进制查看工具看开头几个字节。命令行下hexdump -C yourfile.json | head -3只要开头不是7b就说明有问题。Windows 上可以用各种十六进制编辑器的免费版本或者干脆用在线工具转换一下。找到 BOM 之后处理方式是在编辑器里选择UTF-8 无 BOM另存CSV 文件也做同样处理。注意JMeter 的 CSV Data Set Config 有一个文件编码输入框养成显式填 UTF-8 的习惯。留空的话不同版本、不同操作系统下的默认行为可能不一致这在跨平台协作时特别容易出问题。2.3 参数化数据把脏字符带进了 JSON 中间当你的登录请求开始做参数化变量从 CSV 或者数据库里来问题就从body 里的字符变成了变量值里的字符。这类问题最阴险的地方在于JSON 骨架是你写对的肉眼看结构完美问题藏在变量值内部。举几个真实遇到过的例子。CSV 文件是用 Windows 换行 CRLF 保存的某一行的值在导出时末尾带了一个回车读进来的变量值变成admin\r。如果这个变量被放在字符串中间username:admin\r里的\r是实实在在的控制字符JSON 规范不允许字符串里出现未转义的控制字符U0000 到 U001F 都必须转义解析器读到就会报错。如果这个变量恰好紧挨着结构符号比如拼在password:后面那更惨整个结构被一个回车劈成两行解析器在一行末尾期待逗号或右括号看到的是行尾报错位置可能飘到很远的地方。还有一种是从表格软件导出的 CSV某些字段被加了引号包裹比如admin。如果 JMeter 的 CSV 配置里允许引号数据引号会被剥掉没关系但如果配置不当引号被当作值的一部分读进来你塞进 JSON 后就变成username:admin解析器在第二个引号处懵了。这种报错的位置通常指向某个引号但你看 body 整体又觉得引号数量是偶数因为多出来的引号成对出现。更离谱的是从业务系统导出的测试数据里带制表符、带不间断空格NBSPU00A0。NBSP 在视觉上和普通空格一模一样但它在 JSON 里不是合法的空白字符解析器会报Unexpected character并给出码点 160。这个我曾经追了两个小时因为在 JMeter 的文本框里完全看不出来。脏字符类型十六进制表现处理方式回车 CR0D字符串内非法结构被截断CSV 另存为 LF 换行换行 LF0A同上报错位置飘忽同上制表符 TAB09字符串内非法数据清洗或转义不间断空格 NBSPC2 A0看起来像空格但非法全局替换为普通空格零宽字符E2 80 8B完全不可见正则批量清除处理思路是在参数进入 body 之前做一次清洗用 JSR223 预处理里跑一段 Groovy把不可见字符替换掉比在每个请求里手工排查效率高得多。2.4 字符串拼接没做转义反斜杠和引号打架当登录请求需要动态签名、动态时间戳、或者密码里包含特殊字符时很多人会选择用脚本拼 JSON。拼接本身没问题问题是拼接时不做转义。最典型的场景是密码。测试环境为了覆盖边界情况会设置一些包含特殊字符的密码比如abc123或者abc\123。这些值直接塞进password:${pwd}这种模板里如果密码里含双引号JSON 就变成password:abc123解析器在abc后面的引号处认为字符串结束了接着期待逗号结果看到1报错。如果含反斜杠password:abc\123里\1不是合法转义序列同样报错。反斜杠的问题还出现在路径类参数上。Windows 路径C:\data\file.txt直接放进 JSON 字符串\d和\f都不是合法转义正确的写法必须是C:\\data\\file.txt。这个在配置文件或日志路径参数化时经常踩。解决办法有两个层次。轻量级的做法是在变量替换时手工处理把已知会出问题的字符替换掉。稳妥的做法是根本不用字符串拼接改用脚本构造。Groovy 的JsonBuilder会负责所有转义工作你只管把对象塞进去def payload [ username: vars.get(login_user), password: vars.get(login_pwd), timestamp: vars.get(ts) ] def body new groovy.json.JsonBuilder(payload).toString() vars.put(login_body, body)构造好之后放进变量Body Data 里只写${login_body}。这样无论密码里有什么妖魔鬼怪序列化出来的都是合法 JSON。这个做法我从三年前开始在所有需要动态 body 的项目里用再没在这类问题上浪费过时间。注意JMeter 的函数库里对 JSON 转义的直接支持有限不同版本行为也不完全一致。依赖函数做转义不如依赖 Groovy 的序列化器确定性更强。2.5 请求头 Content-Type 和实际 body 不匹配这一类严格来说不一定会报Unexpected character但值得单独说因为它经常和字符问题混在一起让人判断失误。JMeter 的 HTTP 请求采样器里如果你用了 Body Data 面板填 JSON必须自己在 HTTP 信息头管理器里加一条Content-Type: application/json。JMeter 不会因为你在 Body Data 里写了花括号就自动推断成 JSON。没有这条头服务端可能按application/x-www-form-urlencoded或者text/plain来解析有些框架在这种情况下压根不走 JSON 反序列化你看到的可能是参数解析失败或者空指针而不是 JSON 解析错误。反过来也有坑信息头管理器里写了Content-Type: application/json但同时又在参数面板里填了键值对。JMeter 对这两个面板的处理是互斥的切到 Body Data 之后参数面板会被禁用但如果是从别人那里拿到的 JMX 文件或者手工改过 JMX 的 XML可能出现两边都有内容的情况最终发出去的 body 到底是什么就不确定了。还有一种隐性冲突HTTP 请求采样器里勾选了对 POST 使用 multipart/form-data这个选项会覆盖 Content-Type把整个 body 按 multipart 格式化。这时候你的 JSON 变成了 multipart 的一个 part服务端解析出来是分隔符和头部自然报各种奇怪的字符错误。我见过有人在排查了半天字符问题之后发现自己压根不该勾那个复选框。字符集参数也要留意。Content-Type: application/json不带 charset 时服务端可能按默认编码有些环境是 ISO-8859-1来读 body中文用户名就会变成乱码字节解析器看到一堆高位字节报Unexpected character并给出一个很大的码点。规范写法是Content-Type: application/json;charsetUTF-8把编码钉死。2.6 结构与语法本身的低级错误前面说的都是外部带进来的脏字符这里回归 JSON 语法本身。虽然大部分人第一反应就是查这个但恰恰因为是第一反应反而容易看走眼。尾随逗号是最常见的。{username:admin,password:123456,}最后那个逗号人眼扫过去觉得是分隔符实际上 JSON 规范不允许。数组也一样[a,b,]非法。这种错误在手工编辑、增删字段之后特别容易出现。JSON 里写注释是另一个高频错误。有些人习惯了 JavaScript 的对象字面量顺手写// 登录接口或者/* 临时调试 */这在前端能跑在标准 JSON 里非法解析器读到/直接报错code 47。单引号包裹键或值也是经典错误。{username:admin}在某些宽松的解析器里能过但标准实现不认报Unexpected character ( (code 39))。这种多半是从 Python 代码或者 JS 对象里直接复制过来的。还有undefined、NaN这种字面量从浏览器的控制台复制对象时经常带出来JSON 里只认null。大括号层数错位也值得提一句。有时候从接口文档复制示例文档里为了说明结构外面多包了一层{data: {...}}你直接把整个示例贴进去实际接口要的是里层对象。这种不会报字符错误会报字段缺失但如果外层键名对不上服务端可能报的是别的问题。所以看报错的时候要确认一下报的到底是语法错误还是业务校验错误两者处理路径完全不同。3. 一套五步排查法半小时内定位到具体字符3.1 第一步确认 JMeter 实际发出的 body 长什么样所有排查的起点都是看真东西。不要看你以为的配置要看实际发出的字节。在 JMeter 的查看结果树里选中出错的请求右侧切到请求标签页这里显示的是经过变量替换之后、JMeter 实际准备发送的内容。注意是准备发送对于绝大多数情况已经够用了。如果这一页看到的 body 和你配置的模板不一样变量替换环节就有问题先解决这个。但请求页签有个局限它显示的文本可能经过了一次渲染控制字符和一些不可见字符不一定能看出来。所以更彻底的做法是在测试计划里加一个调试取样器Debug Sampler把关键变量都输出出来看看变量值本身是什么。我一般会配置成输出 JMeter 变量和系统属性然后在结果树里逐个核对${login_user}、${login_pwd}、${login_body}这些值的实际内容和长度。还有一种终极手段在服务端或者中间层抓包。如果能拿到服务端的访问日志看看它收到的原始 body 最好。拿不到的话可以在测试环境里临时起一个简单的接收服务把收到的原始内容打印出来。我做过一个极简的接收端点就打印请求头、请求体长度和前 200 个字节的十六进制装在测试环境里专门用来复现这类问题。有了它JMeter 发什么一目了然不用再猜。3.2 第二步用 Postman 或 curl 做对照实验确认了 JMeter 发的东西下一步是把同样的 body 用另一个工具发一遍看服务端反应是否一致。这个对照实验能帮你把问题范围一刀切开。如果 Postman 发同样的 body 也报错那问题就在 body 本身跟 JMeter 没关系去查字符。如果 Postman 发同样的 body 成功那说明 body 内容没问题差异在请求头、编码、或者 JMeter 特有的发送方式上。做对照实验的时候要注意控制变量只改一个东西。常见的差异点包括Content-Type 带不带 charset、有没有多余的请求头、body 的字节长度是否一致、用的是 HTTP 还是 HTTPS、有没有走代理。我遇到过一个是 JMeter 默认会在请求头里加Connection: keep-alive和User-Agent: Apache-HttpClient/...某些网关对 User-Agent 有特殊处理虽然罕见但排除掉总是好的。对照的时候建议直接从 JMeter 的请求页签把 body 复制出来粘贴到 Postman 的 raw 模式里这样保证两边输入完全一致。反过来也可以在 Postman 里调通之后用它的生成代码功能导出 curl再和 JMeter 的配置对比。两边对不上的地方就是嫌疑点。3.3 第三步把变量冻结成常量做二分定位如果 body 里有参数化变量而你又怀疑问题出在变量值上最快的办法是二分法把所有变量先替换成固定的常量值比如用户名admin、密码123456跑一遍。如果通过说明 JSON 骨架没问题问题在某个变量的值里。然后逐个把变量换回来换到哪个报错就是哪个变量脏了。这个方法看起来笨但极其有效因为它把一个复杂问题变成了若干个简单问题。我一般会先整体替换再对半替换运气好的话两三分钟就能锁定是哪个字段。锁定之后再针对那个字段做深度检查看它的长度、看它的首尾字符、看它的十六进制表示。有个细节要注意替换变量的时候不要只替换值连引号一起替换。因为有些变量是带引号的值有些是不带引号的值模板写法不一样。比如username:${user}和count:${num}前者是字符串后者是数字。如果数字变量为空body 变成count:后面直接跟右括号这属于语法错误报的可能是Unexpected character (} (code 125))或者Unexpected end-of-input。这类问题在变量冻结的时候会立刻暴露出来。3.4 第四步逐字节体检看十六进制当所有常规手段都用完还是找不到就该上十六进制了。把可疑的内容保存成文件用xxd或者hexdump -C看一遍。正常 JSON 的开头应该是7b{后面跟22如果你看到ef bb bf 7b就是 BOM看到20 20 7b说明前面有空格看到27 7b说明前面有个单引号。字符串内部的检查同理。中文用户的输入如果编码不对会看到一堆c3 a4、e4 b8 ad之类的字节。如果 Content-Type 里声明了 UTF-8 但实际发的是 GBK 字节服务端按 UTF-8 解码就会得到乱码字符其中一些可能落在非法区间触发Unexpected character。在 Windows 环境下如果不方便用命令行可以用支持显示控制字符的编辑器。多数现代编辑器都有显示所有字符或者渲染控制字符的选项打开之后回车显示成CR/LF制表符显示成箭头NBSP 显示成一个特殊的点。这比在 JMeter 的小文本框里猜要靠谱得多。提示做十六进制检查时重点看三个位置——body 的最开头、body 的最结尾、以及所有变量替换进来的位置。这三处覆盖了九成的问题。3.5 第五步确认服务端到底期望什么排查到最后如果还是一头雾水很可能是你和服务端对接口的理解不一致。这种情况下再怎么检查自己的 body 也没用得去确认对方要什么。需要确认的信息包括接口接受的 Content-Type 具体是什么有些框架只认application/json带 charset 反而不认虽然少见但存在、body 是必填还是可选、字段名大小写是否敏感、是否有必填字段缺失时的特殊处理、是否存在请求体加密或签名要求。有些系统会对 body 做一层额外的处理比如要求整个 JSON 先 base64 再包一层或者要求带特定格式的签名头这些如果不知道body 写得再标准也是报错。我吃过一次亏某个接口文档写的是 JSON 提交实际服务端还要求 body 里带一个sign字段值是其他字段按字典序拼接后的摘要。缺了这个字段服务端在反序列化之后的校验阶段报错但错误信息被包装成了 JSON 解析失败的样子误导了我很久。所以看报错要看完整堆栈别只看最外层那句话。4. 把登录请求写对可直接抄的配置方案4.1 HTTP 请求与信息头管理器的标准配法说完了排查讲怎么配才是对的。这一套配置我在多个项目里复用基本没再出过字符类问题。HTTP 请求采样器的关键配置配置项取值说明协议https 或 http按被测环境填方法POST登录通常是 POST路径/api/login按实际接口填不要带域名编码UTF-8显式填别留空重定向不自动跟随登录接口通常不需要跟随跳转使用 keep-alive勾选压测时减少连接开销对 POST 使用 multipart不勾选除非接口明确要求表单上传参数 / Body Data只用 Body Data两者互斥别混用HTTP 信息头管理器里至少加两条Content-Type: application/json;charsetUTF-8 Accept: application/json如果接口需要 token 或者特定的客户端标识也在这里加。注意不要在信息头管理器里加Content-LengthJMeter 会自动计算手工加反而容易出错。有一个容易忽略的点如果测试计划里既有需要 JSON 的接口又有需要表单的接口不要把Content-Type加在全局的信息头管理器里应该在每个请求下挂独立的信息头管理器。JMeter 的头管理器作用域是向下继承的全局加会导致所有请求都被强制成 JSON表单接口就废了。这个坑我在早期的脚本里踩过表现为一部分接口莫名其妙报参数缺失查了很久才发现是继承导致的。4.2 请求体模板与参数化写法静态字段直接写死动态字段用变量这是基本原则。一个典型的登录 body{ username: ${login_user}, password: ${login_pwd}, captcha: ${captcha_code}, clientType: web, timestamp: ${ts} }注意timestamp那里没有引号因为它要发数字。这种混排最容易在变量为空时出问题所以要么在预处理器里保证变量一定有值要么在模板里给个兜底。JMeter 支持${ts}和${__P(ts,0)}这类写法后者可以指定默认值用在可能缺失的字段上比较稳。如果 body 结构比较复杂或者需要根据条件动态增减字段就不要在 Body Data 里手写了改用预处理器构造整段 body 放进变量Body Data 只写一个变量引用。这样做的另一个好处是可以在构造时做数据清洗和类型转换把问题挡在发送之前。密码字段要特别注意。测试环境的密码如果包含特殊字符靠模板替换一定会出问题。我的做法是统一在预处理器里处理用 Groovy 的序列化能力构造前面给过示例。如果不想引入脚本至少要在 CSV 里避免使用包含双引号和反斜杠的密码改用其他特殊字符覆盖边界比如!#$%^*这些在 JSON 字符串里是安全的。4.3 编码与文件格式的落地规范编码问题不靠自觉靠规范。我在团队里推的几条硬性要求落地成本很低收益很大。第一所有涉及请求体的文本文件包括 CSV、JSON 模板、脚本文件统一保存为 UTF-8 无 BOM。这条要在编辑器的默认设置里改掉别指望每次手动选。第二JMeter 的jmeter.properties里把sampleresult.default.encoding设成UTF-8保证结果文件里的中文不乱码方便排查。第三CSV Data Set Config 的文件编码字段永远显式填写UTF-8不留空。CSV 文件本身还有几个配置项要注意Filename: login_users.csv File encoding: UTF-8 Variable Names: login_user,login_pwd Delimiter: , Allow quoted data: True Recycle on EOF: True Stop thread on EOF: False Sharing mode: All threadsAllow quoted data这个选项要理解清楚设为 True 时字段外层如果被双引号包裹引号会被剥掉。如果你的密码本身就包含引号并且你希望保留这个设置会带来困扰。我一般的做法是数据文件里不放引号把这个选项当成分隔符兼容开关来用。还有一个跨平台的坑Windows 上编辑的 CSV 默认是 CRLF 换行Linux 环境读的时候最后一个字段可能带上\r。解决办法是在 Windows 侧的编辑器里把换行符改成 LF或者在读取之后做一次清洗。这个问题在做分布式压测、控制机和执行机操作系统不一致的时候特别典型。4.4 动态参数与关联的正确姿势登录请求之后通常要提取 token 给后续接口用这一步如果用错方式也可能间接导致请求体出错。比如用正则提取器提取出来的值带了空格或者换行塞进下一个请求的 body 里就变成脏字符。提取的时候建议用 JSON 提取器表达式写$.data.token勾选计算连接变量能避免多值情况下的取值歧义。提取完在调试取样器里确认一下值的长度和首尾字符如果长度比预期多多半是带上了空白。变量拼接有个函数值得记一下__V用于拼接变量名__eval用于解析变量值里的变量引用和函数。后者在需要二次解析的场景很有用但也容易把不该解析的内容解析了。比如变量值里恰好有${这样的字符串__eval会尝试解析它结果不可预期。这种场景下改用 JSR223 显式处理更安全。还有一个实践建议给关键变量加上断言。比如提取到 token 之后加一个响应断言或者 JSR223 断言检查 token 长度是否在合理范围、是否包含非法字符。问题在源头暴露比在下游某个接口报奇怪的字符错误要好查得多。5. 常见问题速查表与踩坑记录5.1 一张表覆盖八成场景以下是我整理的速查表按报错特征和可能原因对应遇到问题时从上往下逐条排除现象特征最可能原因快速验证方式code 39body 开头是单引号从 curl 整段复制看 body 第一个字符code 239报错位置在开头UTF-8 BOM十六进制看前三字节code 8220 / 65306全角标点用字符可视化工具看code 34位置在字段中间引号嵌套未转义检查含引号的变量值code 92反斜杠未转义检查路径类参数code 47JSON 里写了注释全局搜索//和/*code 125报错在结构末尾尾随逗号或字段缺值检查最后一个字段后是否有逗号报错位置飘忽不定变量里含换行或回车检查数据文件换行符换一个线程就报错参数化数据某行有问题二分法定位具体行浏览器通、JMeter 不通请求头或编码差异对比两边请求头这张表不是穷举但覆盖了我这几年遇到的绝大部分情况。用的时候注意一点不要一次改多个地方改一处跑一次否则即使好了也不知道是哪次改动起的作用下次遇到还是不会。5.2 几个反直觉的坑坑一JMeter 里看着是空的实际有内容。Body Data 面板里如果只有一个空格或者一个换行视觉上和空白几乎没区别但发出去就是非空 body。服务端拿到一个空格解析器跳过空白后发现没有内容可能报的是end-of-input而不是character。但如果是一个不可见的零宽字符就会报character。这种情况建议把 Body Data 全选删掉重新粘贴别用退格键一个个删。坑二改了配置没生效。JMeter 的测试计划是有作用域的如果你改的是全局信息头管理器但某个请求下挂了局部管理器局部会覆盖全局。排查时发现改了没反应先看作用域树。还有一种情况是用了分布式压测改的是控制机的 JMX但执行机上的配置没同步跑起来还是老样子。坑三HTTP 和 HTTPS 混用导致的中间层改写。有些网关会在转发时改写请求体比如把 gzip 压缩的 body 解压后重写或者做字符集转换。这种情况下 JMeter 发出去是好的服务端收到是坏的。验证方法是在服务端入口打日志看原始字节。这个问题排查起来最费劲因为它不在你的控制范围内只能靠沟通。坑四重定向把 POST 变成 GET。登录接口有时候会返回 302JMeter 如果勾了自动跟随重定向会把后续请求变成 GETbody 直接丢了。服务端收到一个没有 body 的请求报的可能是 JSON 解析失败。这种情况看结果树里的子请求就能发现实际发生了两次请求。登录接口一般建议关掉自动重定向把 302 当作正常响应来处理。坑五并发下偶发报错。单线程跑一百遍都好十个线程一起跑就开始报字符错误。这种情况多半是变量共享问题比如用了__counter或者__threadNum拼接 body但拼接方式有问题或者 CSV 的共享模式配置不当多个线程读到了同一个变量的中间状态。排查方法是把线程数降到 2看是否还能复现再逐步加。6. 长期做法让这类问题少发生6.1 请求模板化与调试取样器常驻一次性解决问题靠排查长期少出问题靠工程化。我现在做任何脚本第一件事是把调试取样器加到测试计划里并且设成常驻不因为调通了就删掉。它的开销很小但在出问题时能立刻看到所有变量的实际值省掉大量猜测。第二件事是建立请求模板。登录、查询、提交这类高频接口各做一个标准模板放在一个独立的测试片段里新项目直接引用。模板里把信息头、编码、断言、调试取样器都配好只留业务字段需要填。这样新写的脚本天然带着正确的骨架不会因为某个人忘了配 Content-Type 而浪费时间。模板里我还会加一个 JSR223 断言做基础的合法性检查body 长度是否大于零、是否以{开头、是否以}结尾、是否包含连续两个以上的换行。这几条检查拦不下所有问题但能拦住大部分低级错误而且报错信息清晰直接告诉你是哪个检查没过。6.2 把编码基线写进团队约定技术问题最终都要落到约定上。我在团队里推的编码基线就三条文件统一 UTF-8 无 BOM、接口统一显式声明 charset、数据文件统一 LF 换行。这三条听起来琐碎但每一条背后都是一堆踩过的坑。约定要配上检查手段才有生命力。我们在提交脚本前会跑一个简单的检查脚本扫描 JMX 文件里有没有硬编码的中文、扫描数据文件开头有没有 BOM、扫描 CSV 的换行符。这些检查用几行脚本就能实现跑一次几秒钟但能挡住绝大多数会在联调时爆发的问题。最后分享一个小技巧是我自己用得最顺手的准备一个最小可用登录请求body 里只有用户名和密码两个字段其他全部去掉请求头只留 Content-Type单独存成一个 JMX。任何新环境、新接口出问题先用这个最小请求跑一遍。它能通说明基础链路没问题往复杂了加它不通说明问题在最基础的地方从编码和请求头查起。这个习惯让我在很多次联调里比同事快半步定位到问题也让Unexpected character这种报错从要查一下午变成了看一眼 code 值就知道大概。