这是我参与更文挑战的第2天,活动详情查看: 更文挑战
node转发formdata数据
最近补回了千年老坑:企业微信机器人sdk兼容node,解决的过程有点困难,还好都一一解决了..
背景:由于企业微信上传文件的接口使用的是formdata格式,所以就干脆客户端和作为代理的node代码都需要用formdata格式了。但是吧我又不想用其他库,毕竟就是一个小小的请求而已..自己封装一下得了。于是就开始了我的踩坑之路...
背景
客户端:浏览器环境代码
直接用的是formdata,没啥好说的,代码如下:
form.setAttribute("method", "POST");
if(format === "formdata"){
// 如果要发送formdata类型就直接把页面上的input赋值到form的input中
form.setAttribute("action", "http://localhost:3000/postFile");
form.setAttribute("enctype","multipart/form-data")
let input = document.createElement("input");
input = msg;
form.appendChild(input);
let keyInput = document.createElement("input");
keyInput.name = "rootKey"
keyInput.value = this.key
form.appendChild(keyInput);
}
客户端:node环境代码
需要自己封装formdata了,参考了postman里面的http格式代码,调试了很多次最后得到代码如下(其实是太不对的代码):
const resObj = {
postData: "",
headers: {} as any
}
//格式化消息体
if(format === "formdata"){
//随便来一个key,问题不大
const boundaryKey = "515897053453198140930459"
resObj.postData = getFormData(boundaryKey, '1.txt', msg, this.key)
resObj.headers = {
hostname: 'localhost',
port: 3000,
path: '/postFile',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=--------------------------515897053453198140930459',
'Content-Length': checkLength(resObj.postData)
}
};
}
//封装消息体
export function getFormData(boundaryKey:string, filename: string, msg: string, key: string) {
const extType = getMimeType(filename)
return `----------------------------${boundaryKey}\r
Content-Disposition: form-data; name="media"; filename="${filename}"\r
Content-Type: ${extType}\r
\r
${msg}\r
----------------------------${boundaryKey}\r
Content-Disposition: form-data; name="rootKey"\r
\r
${key}\r
----------------------------${boundaryKey}--\r
`
}
function getMimeType(filename: string){
const mimes = {
'.png': 'image/png',
'.gif': 'image/gif',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.txt': 'text/plain'
};
const extname = filename.match(/\.(\w+)$/)
return extname?mimes[extname[0]]:mimes['.txt'] //默认txt
}
这里边有一些调试了很多次才被我揪出来的坑:
- 字符长度Content-Length不能写太长,写短了消息体会被切短,最好老老实实计算。
- Content-Type一定要写,里面的boundary不能照抄postman,要写很长的-----。
- 这个boundarykey填啥都可以,可以固定不变,不影响请求。
- 注意\r\n,一定要注意,不然会导致消息体解析失败,请求直接发不出来。
- 注意各个key之间的顺序,不然到node端自己手动解析就很麻烦。
格式化好之后发送请求代码如下:
//发送formdata请求
const {headers, postData} = this.getFormatBody(sendMsg[i], format)
const req = http.request(headers, function(res) {
res.setEncoding('utf8');
const resData = [] as any;
res.on('data', function (chunk) {
resData.push(chunk);
});
res.on('end', function() {
allRes.push(resData.join(""))
if(i===msg.length-1){
resolve(allRes);
}
})
});
req.write(postData);
服务端代码
做服务端的时候就有点麻烦了,一开始我是做了一个简单的字符串解析:
const postFile = async (ctx: any) => {
const postMsg = ctx.request
delete postMsg.header.host
// 获取客户端传过来的key值
const keyReg = /(?<="rootKey"\r\n\r\n)(\w|\W)+(?=\r\n-----)/
const keyStr = allBodyArr.pop()
key = keyStr.match(keyReg)[0]
// 发送rawBody
const allBody = postMsg.rawBody //要设置bodyParser的拓展格式的form为formdata才能获取到
// 获取formdata中的分隔符(其实也可以在header中获取)
const splitKey = allBody.match(/------(\w|\W)+?(?=\n)/)[0]
// 获取每一部分消息体
const allBodyArr = allBody.split(splitKey)
// 第一个是换行符,去掉
allBodyArr.shift()
for(let i = 0; i < allBodyArr.length; i++){
// 拼接formdata body
let bodyStr = `\n${splitKey}${allBodyArr[i]}${splitKey}\n`
// 调用企业微信接口,获取文件的id
let fileInfo = await postAxios(`https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=${key}&type=file`,
postMsg.header, bodyStr)
if(fileInfo.errcode=="0"){
// 发送文件
let res = await postAxios(`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`,
null, botSendFile(fileInfo["media_id"]))
if(res.errcode!="0"){
ctx.body = sendText(0, 200, "发送文件失败")
}
}else{
ctx.body = sendText(0, 200, "获取文件id失败")
}
ctx.body = sendText(0, 200, "ok")
}
}
这时候其实是可以跑起来的,但是多发几个文件就会发现问题:
- 文本文件可以正常传递,但是传输文件和图片出现问题,发送到企业微信上的图片,下载下来发现不符合规范。
- 而且有些小图片明明符合长度要求,客户端却报错。
找bug
先从请求入手,发现postmsg,node,浏览器三个方法得到的都一样,是错误的文件格式,于是确认时后端代码的问题。其实我对自己封装的东西也没啥自信,怀疑是写多了什么或者不小心弄少了什么字符,然后我拿到无法显示的图片进行保存,右键以文本的形式打开,然后再把原来的图片用文本格式打开,进行对比。发现无法显示的图片是utf-8编码,原来的图片并不是这个编码方式。是bodyparser直接把数据转换成utf-8格式了,查了一下,文档没有说怎么获取buffer格式。
好吧,所以还是要我自己来想办法处理
解决思路
-
思路1:保存后转发。网上的博客一搜全都是这种方法,考虑过要不要用,但是还是觉得太累赘。
-
思路2:直接转发。
其实我用网上的formdata库试了一下,是可以直接把读取到的文件用axios进行转发的,想不通为啥,就去看了一下源码,发现人家使用的是流的形式,于是思考怎么用流的方式去做转发...
- 想去配置代理直接转发客户端请求,查了半天太麻烦了。
- 然后去看pipe啥的能不能做转发,好像没啥用,怎么转发都是依赖数据格式的,最终还是这个数据格式的问题。
- 最后又想怎么获取到原生的请求体。
解决问题
- 发现ctx.req是node的原生请求,于是用req.on监听得到原生buffer数据。试了一下axios能不能发送,嘿老天不负有心人,居然还真的可以,于是解决问题。
- 解决之后发现之前的图片等请求出了问题,发现还是需要bodyparser,于是把转buffer这一过程以中间件的形式写在bodyparser前面,挂到req的属性上。
最终代码如下:
//中间件代码
app.use(async (ctx, next) => {
//处理formdata格式数据为buffer类型
if (ctx.request.url === '/postFile') {
const buffer: any = []
ctx.req.on('data', (chunk: any) => {
buffer.push(chunk)
})
ctx.req.on('end', (chunk: any) => {
const bufferRes = Buffer.concat(buffer)
const keyReg = /(?<="rootKey"\r\n\r\n)(\w|\W)+(?=\r\n-----)/
const key = bufferRes.toString('utf-8').match(keyReg)[0]
ctx.myrequest = {
bufferRes,
key,
}
})
}
}))
await next()
}).use(
bodyParser({
formLimit: '20mb',
extendTypes: {
form: ['multipart/form-data'], // will parse application/x-javascript type body as a JSON string
},
}),
)
//发送文件代码
const postFile = async (ctx: any) => {
const postMsg = ctx.request
delete postMsg.header.host
const {bufferRes, key} = ctx.myrequest
let fileInfo = await postAxios(
`https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=${key}&type=file`,
postMsg.header,
bufferRes,
)
if (fileInfo.errcode == '0') {
// 发送文件
let res = await postAxios(
`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`,
null,
botSendFile(fileInfo['media_id']),
)
if (res.errcode != '0') {
ctx.body = sendText(0, 200, '发送文件失败')
}
} else {
ctx.body = sendText(0, 200, '获取文件id失败')
}
ctx.body = sendText(0, 200, '发送文件成功')
}
成功解决~
客户端node环境代码问题
post和浏览器搞定了,发现node中自己封装的formdata还是有编码问题,因为我是直接用字符串拼接的
解决:把代码分成头部,消息体,和尾部。
-
消息体由于是直接读取的文件,本来就是buffer
-
头部和尾部都是字符串,所以都要转为buffer。
用Buffer.from(head/tail)即可。
-
最后用Buffer.concat把三部分进行拼接,用Buffer.byteLength(allBuffer)计算字节长度
代码如下:
const boundaryKey = "515897053453198140930459"
//要用buffer避免编码问题
export function getFormData(filename: string, msg: any, key: string) {
const extType = getMimeType(filename)
const head = `----------------------------${boundaryKey}\r
Content-Disposition: form-data; name="media"; filename="${filename}"\r
Content-Type: ${extType}\r
\r\n`
const tail = `\r
----------------------------${boundaryKey}\r
Content-Disposition: form-data; name="rootKey"\r
\r
${key}\r
----------------------------${boundaryKey}--\r
`//全都转为buffer
const headBuffer = Buffer.from(head)
const tailBuffer = Buffer.from(tail)
const allBuffer = Buffer.concat([headBuffer, msg, tailBuffer])
return {
body: allBuffer,
head: {
'Content-Type': `multipart/form-data; boundary=--------------------------${boundaryKey}`,
'Content-Length': Buffer.byteLength(allBuffer)
}
}
}
解决问题~~