背景介绍
但是如果把微信的接口换成自己的,都可以成功接收到数据,所以肯定是用 axios 和 request 发送 HTTP 请求的时候,有一些区别。那如何找到这个区别呢,只能通过抓包了。
抓包排查
首先打开 Charles,然后分别运行 axios 和 request 的代码,注意,要在代码里面配上代理,我们看下 request 的代码:
var request = require('request')
var fs = require('fs')
var options = {
method: 'POST',
url:
'https://api.weixin.qq.com/wxa/img_sec_check?access_token=xxx',
headers: {
'Content-Type': 'application/json',
},
formData: {
contentType: 'image/jpeg',
value: fs.createReadStream('/Users/keliq/Pictures/jzm.jpeg'),
},
proxy: 'http://127.0.0.1:8888',
rejectUnauthorized: false,
}
request(options, function (error, response) {
if (error) throw new Error(error)
console.log(response.body)
})
抓包结果如下:
-
请求
POST /wxa/img_sec_check?access_token=xxx HTTP/1.1 Content-Type: multipart/form-data; boundary=--------------------------630489123810205833486663 host: api.weixin.qq.com content-length: 11998 Connection: close ----------------------------630489123810205833486663 Content-Disposition: form-data; name="contentType" image/jpeg ----------------------------630489123810205833486663 Content-Disposition: form-data; name="value"; filename="jzm.jpeg" Content-Type: image/jpeg ÿØÿà...
-
响应
HTTP/1.1 200 OK Date: Mon, 05 Apr 2021 06:59:26 GMT Content-Type: application/json; encoding=utf-8 RetKey: 11 LogicRet: 42001 Content-Length: 81 Connection: close {"errcode":42001,"errmsg":"access_token expired rid: 606ab54e-4b90bc8a-66e6fc25"}
再看 axios 的代码
var axios = require('axios')
var FormData = require('form-data')
var fs = require('fs')
var data = new FormData()
data.append('contentType', 'image/jpeg')
data.append('value', fs.createReadStream('/Users/keliq/Pictures/jzm.jpeg'))
var config = {
method: 'post',
url:
'https://api.weixin.qq.com/wxa/img_sec_check?access_token=xxxx',
headers: {
'Content-Type': 'application/json',
...data.getHeaders(),
},
data: data,
proxy: {
host: '127.0.0.1',
port: 8888,
},
}
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data))
})
.catch(function (error) {
console.log(error)
})
抓包结果如下:
-
请求
POST /wxa/img_sec_check?access_token=xxx HTTP/1.1 Accept: application/json, text/plain, */* Content-Type: multipart/form-data; boundary=--------------------------571426341398317845770943 User-Agent: axios/0.21.1 host: api.weixin.qq.com Transfer-Encoding: chunked Connection: close ----------------------------571426341398317845770943 Content-Disposition: form-data; name="contentType" image/jpeg ----------------------------571426341398317845770943 Content-Disposition: form-data; name="value"; filename="jzm.jpeg" Content-Type: image/jpeg ÿØÿà...
-
响应
HTTP/1.1 412 Precondition Failed Date: Mon, 05-Apr-2021 07:00:41 GMT Content-Length: 0 Connection: close
经过对比发现,axios 比 request 多了一个 Transfer-Encoding 头部,值为 chunked,而 request 比 axios 多了一个 Content-Length 头部,因此这两个头部的区别才是导致微信接口返回 412 的元凶。
了解 Content-Length 和 Transfer-Encoding 头
网上查了很多资料,发现了朴瑞卿的这一篇文章 里面说得非常详细,在此摘录部分内容如下:
- Content-Length:表示 HTTP 消息长度,用十进制数字表示的八位字节的数目
- Transfer-Encoding:如果在请求处理完成前无法获取消息长度,此时应该使用 Transfer-Encoding: chunked
Content-Length 的工作原理
Content-Length 应该是精确的,否则就会导致异常,这个大小是包含了所有内容编码的,比如对文本文件进行了 gzip 压缩的话,Content-Length 首部指的就是压缩后的大小而不是原始大小。那如果提供的数值不准确会怎样呢?
- Content-Length > 实际长度:服务端/客户端读取到消息结尾后会等待下一个字节,会无响应直到超时。
- Content-Length < 实际长度:第一次消息被截断,第二次解析混乱。
Transfer-Encoding 的工作原理
数据以一系列分块的形式进行发送,在每一个分块的开头需要添加当前分块的长度,以十六进制的形式表示,后面紧跟着 \r\n
,之后是分块本身,后面也是 \r\n
,终止块是一个常规的分块,不同之处在于其长度为 0,图示如下:
抓包后的结果:
解决方案
找到问题就好办了,第一反应就是在 axios 的拦截器中把 Transfer-Encoding 头干掉,然后把 Content-Length 头给加上:
axios.interceptors.request.use(
config => {
console.log('config', config.headers)
delete config.headers['Transfer-Encoding']
return config
},
error => Promise.reject(error)
)
然而还是太天真了,Transfer-Encoding 根本干不掉,后来发现在Content-Length 和 Transfer-Encoding 这两个头部是不共存的,也就是说,如果我手动增加了 Content-Length 头,会自动干掉 Transfer-Encoding 头...
那如何获取 Content-Length 长度呢?最后在 cnodejs 社区竟然发现了一模一样的问题,作者深入研究 request、axios 和 form-data 源码,最终找到了解决方案,就是利用 form-data 的 getLength 方法获取长度,然后手动添加 Content-Length 头。
最后改造后的代码如下:
var axios = require('axios')
var FormData = require('form-data')
var fs = require('fs')
async function request() {
var data = new FormData()
data.append('contentType', 'image/jpeg')
data.append('value', fs.createReadStream('/Users/keliq/Pictures/jzm.jpeg'))
var len = await new Promise((resolve, reject) => {
return data.getLength((err, length) => (err ? reject(err) : resolve(length)))
})
var config = {
method: 'post',
url: 'https://api.weixin.qq.com/wxa/img_sec_check?access_token=xxx',
headers: {
'Content-Type': 'application/json',
...data.getHeaders(),
'Content-Length': len,
},
data: data,
proxy: {
host: '127.0.0.1',
port: 8888,
},
}
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data))
})
.catch(function (error) {
console.log('error')
})
}
request()
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!