起因

在一次需求开发自测中遇到一个问题,一个接口的cateId3List参数中有中括号(’[‘、’]’),是url特殊字符,但请示时传参未做url编码转义,在测试环境会导致返回400,线上环境会概率性的400 Bad Request(和nginx层无关)。
image.png
ps.图书项目使用的是axios,这个接口在发出时也没做过多的封装(可以理解成和用axios发出效果是一样的),下面都是以axios.get的方式为例(版本0.19.2)

问题定位

当时为了不阻塞测试流程,就先让QA通过在fiddler配置规则绕过了这个问题,利用工具的rewrite功能。
image.png
这里做的事就是把请求参数的[]给剔除掉。但是在开发debug时,我们的传入参数明明是不需要提前编码的呀,

  1. const axios = require('axios');
  2. const argString = '你好';
  3. axios.get('/zzopen/sellbook/searchDefaultWord', {
  4. params: {
  5. arg: argString
  6. }
  7. }).then(function (response) {
  8. console.log(response);
  9. })

实际发出去的请求,你好 已经被编码成 %E4%BD%A0%E5%A5%BD,看着都理所当然
image.png
接下来,我们修改一下代码:

  1. const axios = require('axios');
  2. const argString = JSON.stringify(["你", "好"]);//"["你","好"]"
  3. axios.get('/zzopen/sellbook/searchDefaultWord', {
  4. params: {
  5. arg: argString
  6. }
  7. }).then(function (response) {
  8. console.log(response);
  9. })

在看下请求:
image.png
发现和我们预想的不太一样,原本认为应该编码成 %5B%22%E4%BD%A0%22%2C%22%E5%A5%BD%22%5D ,而实际上确是[%22%E4%BD%A0%22,%22%E5%A5%BD%22],这不符合我们的预期呀,只编码了[]中间的内容,而中括号直接pass了。带着疑问我看了下源码(https://github.com/axios/axios),最终找到了关键性的代码buildURL.js中,
image.png
image.png
在get请求传的参数会在这里构造后拼接到url后,在进行encodeURIComponent后,会在把一些encode后的的通过正则的方式在还原成了原本的字符了。What ?

为什么

看了下提交历史,这块修改的代码首次提交时间2015年7月份,看来是个历史悠久的问题。
image.png
但是为什么要加上这些代码呢,提交注释里只有don’t escape square brackets。当问题无法在继续下手时,就需要去看看URI(URL只是URI的子集)的规范了(其实是谷歌的🤦)。这里要说的rfc3986(主要这个里原因),内容比较多感兴趣的可以到这里查看https://tools.ietf.org/html/rfc3986 ,也有中文的 http://wiki.jabbercn.org/RFC3986 。在规范中提到Url只允许包含四种大类的字符(规范里称非保留字符):

  1. 英文字母(a-zA-Z)
  2. 数字(0-9)
  3. -_.~ 4个特殊字符
  4. 保留字符,RFC3986中指定了保留字符(英文字符)为:! * ' ( ) ; : @ & = + $ , / ? # [ ]

看到这里大概就明白了,[]是可以不用在进行编码的(其实编码了也没啥问题)。
在说说这个规范谁制定的,ietf,主要任务是负责互联网相关技术标准的研发和制定,是国际互联网业界具有一定权威的网络相关技术研究团体。说白了互联网标准就是这货制定的,用前端的js场景比喻的话,就有点类似TC39,rfcxxx类似mdn的文档。这个规范在2005年就制定了,为啥到现在还没有完全普及,说到这就和前端的场景更像了,虽然都是标准,但是别人可以不实现,晚实现(eg.浏览器对各语法的支持情况)。
还有疑惑未解,上面还提到了概率性的400 Bad Reques,这个由于啥原因呢,想要得到答案只能从后端下手了,后面和运维同学沟通了解到 只对个别机器的tomcat做过临时的兼容配置,到这里答案就明了。
后端的处理就是修改Tomcat提供的配置字段(conf/catalina.properties),让其兼容需要的字符:
image.png

我们要怎么改

这里撇开服务端去修改Tomcat的配置,来谈谈前端去如何做。

  • 修改请求为post

当时遇到这个问题时,没时间过多的去追究原因,就改用post请求来规避了这个问题,现在回看下,post方式规避了axios对参数的encode。

  • 修改axios源码

关于这一点,在GitHub上有人提了Pull request #2563 提了,但是最终被关闭未能合并到主分支上。如果要修改的话,可以这样写 commit id
image.png

  • 参数直接拼接URL上

从axios源码上看,把请求参数拼接到URL上,其不会再对处理,像这样

  1. const axios = require('axios');
  2. const argString = JSON.stringify(["你", "好"]);//"["你","好"]"
  3. axios.post('/zzopen/sellbook/searchDefaultWord?arg=' + encodeURIComponent(argString))
  4. .then(function (response) {
  5. console.log(response);
  6. })

可以看到参数正常编码了
image.png

  • 使用paramsSerializer处理

axios给出的建议如果参数不满足默认的编码方式的话,可以通过paramsSerializer进行自定义编码(序列化),这种方式还是值得推荐的,能够一劳永逸。

  1. const axios = require('axios');
  2. axios.defaults.paramsSerializer = (params) => {
  3. return Object.keys(params).filter(it => {
  4. return params.hasOwnProperty(it)
  5. }).reduce((pre, curr) => {
  6. return params[curr] ? (pre ? pre + '&' : '') + curr + '=' + encodeURIComponent(params[curr]) : pre;
  7. }, '');
  8. };
  9. const argString = JSON.stringify(["你", "好"]);//"["你","好"]"
  10. axios.get('/zzopen/sellbook/searchDefaultWord', {
  11. params: {
  12. arg: argString,
  13. }
  14. }).then(function (response) {
  15. console.log(response);
  16. })


最后

纸上得来终觉浅,绝知此事要躬行。自勉!