7年后端老炮栽倒在 GET 请求下

138 阅读5分钟

一、前言

产品在群里转发了聊天记录,记录里运营说在 CRM H5端上传图片一直转圈,并附上操作视频。

image.png

第一时间,让测试同学用测试账号在线上进行复现,发现一切正常,并没有出现上传一直转圈的现象。

前端在这块没有加埋点,无法排查;反馈的运营并不在总部,无法当面沟通,了解现场咋情况。

考虑到可能是图片问题,我们将测试图片让运营在其机子上再尝试下,现状依旧。


排查过程

那就从 后端 开始排查喽。

1、🦾使用 sls日志查询,查询对应服务中操作日志,运营同学上传请求

发现并没有上传接口(/file/policy)的日志,但其他操作日志均有。

fan.gif


2、👾向上排查,网关是否有数据

发现网站有接收到请求,且对应的 IP 也是。

image.png

那就根据这个对应 traceid 链式追踪下。

发现居然没有对应服务日志

image.png

正常应该会有如上两条记录,但现在没有,那就是有问题的。

image1.png


3、🫶链路分析:网关有请求数据、应用服务没请求数据

现象:网关有请求数据、应用服务没请求数据

说明:网络没啥问题,有请求打过来,但网关 -> 应用服务这中间出现了问题。

🫵那么可能出现问题点的地方可分为:

  1. ❌转发问题:网关转发到应用服务,请求丢失了。

    1. 但线上其他请求均正常,也没有关于这个的告警。难道会针对IP作路由转发?这个先排除。
  2. 🤔网关问题:请求在网关层就直接返回了,没有往下传递。这个待排查。❓

  3. 🤔应用服务问题:请求打到应用服务了,但服务没接收直接返回了。❓

🤖既然有请求,能否得到更多的请求信息呢,比如:请求携带的参数等。

后台服务请求链路如下:

image1.png

再往上层排查,可以查询 ingress日志:

image.png

可以看到日志中完整报文信息,主要注意如下两项参数

  • HTTP status 状态码:400,说明是请求参数异常。
  • URL 数据:/api/file/policy?fileName=wx_camera_1750406076223_[B%407a35589.jpg&fileSize=167802&clientId=d5c68bd9fa004ab4a8581153fbf32ba8

可以发现 fileName 字段的数据格式有些异常,可能有问题。

那么接下来就是模拟这个请求。


4、🤡模拟请求:罪魁祸首是 "["

本地模拟请求:可以复现,现象跟线上一致。

image1.png

多次测试,发现:

  • 带有 “ [ ” 的 URL 参数,服务会报 400 异常
  • 去掉 " [ " 的 URL 参数,服务正常返回

那么现在就要排查是网关还是应用服务出现问题,先在应用服务里看是否有对应的 ERROR 日志。

缩小时间范围 + 请求复现,我们发现在应用服务中有如下日志:

image.png

因为日志级别是 INFO,所以没有告警出来,并且淹没在茫茫日志系统中。

这个错误也说明了:

  1. 请求URL包含非法字符:URL中包含了RFC 7230和RFC 3986标准不允许的字符

  2. 常见的非法字符包括:

    • 中文字符或其他非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'
};

这也说明了,为什么测试机上可行。


二、解决

解决方式主要两种

  1. 前端主动对 URL 上的 fileName 进行 URI encoding,后端接收到数据需要 URI decoding。
  2. ❤️换请求方式:GET 请求改为 POST请求,参数放到 BODY 里
@ApiOperation("获取上传 Policy")
@GetMapping("/file/policy")
public FilePolicyDTO getPolicy(@RequestParam String fileName,
                                @RequestParam Integer fileSize,
                                @RequestParam String clientId) {
  
![9224bb8c711741a1aaf853d062815c6c.gif](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/da595b80ce0647de9a6d79e3e6f993b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5qC85qC85q2l5YWl:q75.awebp?rk3s=f64ab15b&x-expires=1754894833&x-signature=d7S%2FCE6dqHrjh17I5UPTA2bkp50%3D)
    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。

9224bb8c711741a1aaf853d062815c6c.gif

当时设计API时会考虑:符合 HTTP 语义和 REST 原则

  1. REST 原则:获取资源使用 GET 请求;主要考虑点:安全性、幂等性、可缓存。
  2. HTTP 语义:RFC 7231 规范中提到:
  • GET 请求的语义是"获取资源"
  • 请求的参数应该通过 URL 查询字符串传递
  • BODY 在 GET 请求中没有定义的语义

GET 请求会有一些限制

  • GET 请求受 URL 长度限制(通常 2048 字符)
  • 大多数服务器和工具忽略 GET 的 BODY

类似往期文章: