一、前言
产品在群里转发了聊天记录,记录里运营说在 CRM H5端上传图片一直转圈,并附上操作视频。
第一时间,让测试同学用测试账号在线上进行复现,发现一切正常,并没有出现上传一直转圈的现象。
前端在这块没有加埋点,无法排查;反馈的运营并不在总部,无法当面沟通,了解现场咋情况。
考虑到可能是图片问题,我们将测试图片让运营在其机子上再尝试下,现状依旧。
排查过程
那就从 后端 开始排查喽。
1、🦾使用 sls日志查询,查询对应服务中操作日志,运营同学上传请求
发现并没有上传接口(/file/policy)的日志,但其他操作日志均有。
2、👾向上排查,网关是否有数据
发现网站有接收到请求,且对应的 IP 也是。
那就根据这个对应 traceid 链式追踪下。
发现居然没有对应服务日志。
正常应该会有如上两条记录,但现在没有,那就是有问题的。
3、🫶链路分析:网关有请求数据、应用服务没请求数据
现象:网关有请求数据、应用服务没请求数据
说明:网络没啥问题,有请求打过来,但网关 -> 应用服务这中间出现了问题。
🫵那么可能出现问题点的地方可分为:
-
❌转发问题:网关转发到应用服务,请求丢失了。
- 但线上其他请求均正常,也没有关于这个的告警。难道会针对IP作路由转发?这个先排除。
-
🤔网关问题:请求在网关层就直接返回了,没有往下传递。这个待排查。❓
-
🤔应用服务问题:请求打到应用服务了,但服务没接收直接返回了。❓
🤖既然有请求,能否得到更多的请求信息呢,比如:请求携带的参数等。
后台服务请求链路如下:
再往上层排查,可以查询 ingress
日志:
可以看到日志中完整报文信息,主要注意如下两项参数:
- HTTP status 状态码:400,说明是请求参数异常。
- URL 数据:/api/file/policy?fileName=wx_camera_1750406076223_[B%407a35589.jpg&fileSize=167802&clientId=d5c68bd9fa004ab4a8581153fbf32ba8
可以发现 fileName 字段的数据格式有些异常,可能有问题。
那么接下来就是模拟这个请求。
4、🤡模拟请求:罪魁祸首是 "["
本地模拟请求:可以复现,现象跟线上一致。
多次测试,发现:
- 带有 “ [ ” 的 URL 参数,服务会报 400 异常
- 去掉 " [ " 的 URL 参数,服务正常返回
那么现在就要排查是网关还是应用服务出现问题,先在应用服务里看是否有对应的 ERROR 日志。
缩小时间范围 + 请求复现,我们发现在应用服务中有如下日志:
因为日志级别是 INFO,所以没有告警出来,并且淹没在茫茫日志系统中。
这个错误也说明了:
-
请求URL包含非法字符:URL中包含了RFC 7230和RFC 3986标准不允许的字符
-
常见的非法字符包括:
- 中文字符或其他非ASCII字符
- 未编码的特殊字符(如空格、
{}
、[]
、|
、``、^
、~
等) - 控制字符
Tips:Tomcat 接收到这特殊字符就直接返回了,并没有忘业务代码走了。
5、💀最后一个问题:同个文件不同机子上传就不行
因为手机端是 H5 应用,不同机子内置的浏览器不同,有些机子会对 URL 上的特殊字符进行 uri encoding。
不同手机使用不同的浏览器内核:
- Android: Chrome内核、系统WebView、厂商定制浏览器
- iOS: Safari内核(WKWebView)
- 其他: UC、QQ浏览器等第三方内核
某些字符在不同浏览器中处理方式不同:
// 容易被编码的字符
const sensitiveChars = {
'中文': '%E4%B8%AD%E6%96%87',
' ': '%20',
'+': '%2B',
'&': '%26',
'=': '%3D',
'#': '%23'
};
这也说明了,为什么测试机上可行。
二、解决
解决方式主要两种:
- 前端主动对 URL 上的 fileName 进行 URI encoding,后端接收到数据需要 URI decoding。
- ❤️换请求方式:GET 请求改为 POST请求,参数放到 BODY 里
@ApiOperation("获取上传 Policy")
@GetMapping("/file/policy")
public FilePolicyDTO getPolicy(@RequestParam String fileName,
@RequestParam Integer fileSize,
@RequestParam String clientId) {

return this.fileService.getPolicy(clientId, fileSize, fileName);
}
@ApiOperation("获取上传 Policy(POST 支持)")
@PostMapping("/file/policy")
public FilePolicyDTO getPolicyPost(@RequestBody FilePolicyReq req) {
return this.fileService.getPolicy(req.getClientId(), req.getFileSize(), req.getFileName());
}
三、小结
现在回想,这个问题挺愚蠢的,也被自己大意蠢哭了。
最细小的,也可能最容易忽略的。面对这种小点最好集成进框架,之后就不用每次 check。
当时设计API时会考虑:符合 HTTP 语义和 REST 原则
- REST 原则:获取资源使用 GET 请求;主要考虑点:安全性、幂等性、可缓存。
- HTTP 语义:RFC 7231 规范中提到:
- GET 请求的语义是"获取资源"
- 请求的参数应该通过 URL 查询字符串传递
- BODY 在 GET 请求中没有定义的语义
GET 请求会有一些限制:
- GET 请求受 URL 长度限制(通常 2048 字符)
- 大多数服务器和工具忽略 GET 的 BODY
类似往期文章:
- 一次 POST请求发送多个请求:juejin.cn/post/693860…
- 抓包文件上传:juejin.cn/spost/69914…
- 从后端角度看大文件上传:juejin.cn/po