HTTP 411 请求错误
背景
- 服务端有一段将用户提交的建议反馈转发给第三方系统的代码,建议反馈中包含图片。
- 测试服上可以正常工作,正式服上某层Nginx代理上返回了411 Length Required。
最小可复现代码
'use strict'
import FormStream from 'formstream'
import urllib from 'urllib'
import {
basename
} from 'path'
async function request(){
const formData = FormStream()
const filepaths = ['a.png', 'b.png']
for (const filepath of filepaths) {
formData.file(basename(filepath), filepath)
}
const params = {
a: 'xxx',
b: 'xxx',
}
for (const key of Object.keys(params)) {
formData.field(key, params[key])
}
const url = 'http://127.0.0.1:7001/api/test'
const options = {
method: 'post',
dataType: 'json',
stream: formData,
headers: formData.headers()
}
return urllib.curl(url, options)
}
原因分析
nginx 日志记录发现Content-Length的长度不正确 查看formstream的源代码,只有在_isAllStreamSizeKnown的时候才会向headers里添加Content-Length
FormStream.prototype.headers = function (options) {
var headers = {
'Content-Type': 'multipart/form-data; boundary=' + this._boundary
};
// calculate total stream size
this._contentLength += this._knownStreamSize;
// calculate length of end padding
this._contentLength += this._endData.length;
if (this._isAllStreamSizeKnown) {
headers['Content-Length'] = String(this._contentLength);
}
if (options) {
for (var k in options) {
headers[k] = options[k];
}
}
return headers;
};
修改后的代码
做以下修改
// ...
for (const filepath of filepaths) {
const fileStat = await promisify(fstat)(filepath)
formData.file(basename(filepath), filepath, fileStat.size)
}
// ...
结果这个请求一直没有发送出去,请求超时
原因分析
对着源码分析了一遍没有找出问题,google后发现了大佬的文章 源码中每次执行完添加数据的代码后都有这么一段
process.nextTick(this.resume.bind(this));
对于resume,有
// ...
FormStream.prototype.drain = function () {
this._emitBuffers();
var item = this._streams.shift();
if (item) {
this._emitStream(item);
} else {
this._emitEnd();
}
return this;
};
FormStream.prototype.resume = function () {
this.paused = false;
if (!this._draining) {
this._draining = true;
this.drain();
}
return this;
};
结论
formstream在调用field之类的函数后会注册一个微任务 微任务执行时会使用流开始发送数据,数据发送完毕后关闭流 因为在调用urllib之前还注册了一个微任务,导致urllib.request实际上是在这个微任务内部执行的 也就是说在request执行的时候,流已经关闭了,一直拿不到数据,所以就抛出异常,提示接口超时。
正常工作的代码
'use strict'
import FormStream from 'formstream'
import urllib from 'urllib'
import {
basename
} from 'path'
import { fstat } from 'fs'
import { promisify } from 'util'
async function request(){
const filepaths = ['a.png', 'b.png']
const fileSizes = {}
for(const filepath of filepaths){
const fileStat = await promisify(fstat)(filepath)
fileSizes[filepath] = fileStat.size
}
const formData = FormStream()
for (const filepath of filepaths) {
formData.file(basename(filepath), filepath, fileSizes[filepath])
}
const params = {
a: 'xxx',
b: 'xxx',
}
for (const key of Object.keys(params)) {
formData.field(key, params[key])
}
const url = 'http://127.0.0.1:7001/api/test'
const options = {
method: 'post',
dataType: 'json',
stream: formData,
headers: formData.headers()
}
return urllib.curl(url, options)
}