我的快递服务器接收来自浏览器的文件上传。 上传内容将作为multipart/form-data
请求进行传输;我使用多方来解析传入的实体正文。
多方允许您获取一个部分(大致是单个表单字段,如<input type="file">
)作为可读流。 我不想在我的 Web 服务器上处理或存储上传的文件,所以我只是将上传的文件部分通过管道传输到对另一个服务的请求中(使用请求模块)。
app.post('/upload', function(req, res) {
var form = new multiparty.Form();
form.on('part', function(part) {
var serviceRequest = request({
method: 'POST',
url: 'http://other-service/process-file',
headers: {
'Content-Type': 'application/octet-stream'
}
}, function(err, svcres, body) {
// handle response
});
part.pipe(serviceRequest);
});
form.parse(req);
});
这在大多数情况下都能正常工作。 节点会自动应用分块传输编码,当浏览器上传文件字节时,它们将作为原始实体正文(没有分段格式)正确发送到后端服务,最终获取完整文件并成功返回。
但是,有时请求会失败,我的回调会使用以下err
调用:
TypeError: The header content contains invalid characters
at ClientRequest.OutgoingMessage.setHeader (_http_outgoing.js:360:11)
at new ClientRequest (_http_client.js:85:14)
at Object.exports.request (http.js:31:10)
at Object.exports.request (https.js:199:15)
at Request.start (/app/node_modules/request/request.js:744:32)
at Request.write (/app/node_modules/request/request.js:1421:10)
at PassThrough.ondata (_stream_readable.js:555:20)
at emitOne (events.js:96:13)
at PassThrough.emit (events.js:188:7)
at PassThrough.Readable.read (_stream_readable.js:381:10)
at flow (_stream_readable.js:761:34)
at resume_ (_stream_readable.js:743:3)
at _combinedTickCallback (internal/process/next_tick.js:80:11)
at process._tickDomainCallback (internal/process/next_tick.js:128:9)
我无法解释该错误来自何处,因为我只设置了Content-Type
标头,并且堆栈不包含我的任何代码。
为什么我的上传偶尔会失败?
此示例演示如何将文件作为文件名中带有国家/地区符号的附件发送。
const http = require('http');
const fs = require('fs');
const contentDisposition = require('content-disposition');
...
// req, res - http request and response
let filename='totally legit .pdf';
let filepath = 'D:/temp/' + filename;
res.writeHead(200, {
'Content-Disposition': contentDisposition(filename), // Mask non-ANSI chars
'Content-Transfer-Encoding': 'binary',
'Content-Type': 'application/octet-stream'
});
var readStream = fs.createReadStream(filepath);
readStream.pipe(res);
readStream.on('error', (err) => ...);
如果请求中有任何字符串,则在发出传出 HTTP 请求时,节点会抛出该TypeError
headers
选项对象包含基本 ASCII 范围之外的字符。
在这种情况下,似乎在请求上设置了Content-Disposition
标头,即使从未在请求选项中指定它。 由于该标头包含上载的文件名,因此如果文件名包含非 ASCII 字符,则可能导致请求失败。 即:
POST /upload HTTP/1.1
Host: public-server
Content-Type: multipart/form-data; boundary=--ex
Content-Length: [bytes]
----ex
Content-Disposition: form-data; name="file"; filename="totally legit .pdf"
Content-Type: application/pdf
[body bytes...]
----ex--
然后,对other-service/process-file
的请求失败,因为多方将部件标头存储在part
对象上,该对象也是表示部件主体的可读流。 当您将part
pipe()
到serviceRequest
中时,请求模块会查看管道流是否具有headers
属性,如果有,则将它们复制到传出请求标头。
这会导致传出请求如下所示:
POST /process-file HTTP/1.1
Host: other-service
Content-Type: application/octet-stream
Content-Disposition: form-data; name="file"; filename="totally legit .pdf"
Content-Length: [bytes]
[body bytes...]
。除了该节点在Content-Disposition
标头中看到非 ASCII 字符并抛出。 抛出的错误被请求捕获,并作为err
传递给请求回调函数。
通过在将部件标头通过管道传递到请求中之前删除部件标头,可以避免此行为。
delete part.headers;
part.pipe(serviceRequest);
就像之前@arrow cmt 一样,在 Content-disposition 标头上使用 encodeURI(文件名)。在客户端中,使用 decodeURI 方法进行解码。
您可以使用encodeURI服务器端和decodeURI客户端。
使用CSV 文件的示例,使用 Express 服务器和 JavaScript 客户端。
服务器
router.get('urlToGetYourFile', async (req, res, next) => {
try {
const filename = await functionToGetYourFilename();
const file = await functionToGetYourFile();
res
.status(200)
.header({
'content-Type': 'text/csv'
'content-disposition': 'attachment;filename=' + encodeURI(filename)
})
.send(file.toString('binary'));
} catch(error) {
return res.status(500).send({ error });
}
}
客户
const getFile = async () => {
try {
const response = await axios.get('urlToGetYourFile');
const filename = decodeURI(response.headers['content-disposition'].split('filename=')[1]);
const type = { type: 'text/csv' };
const blob = new Blob([response.data], type);
return new File([blob], filename, type);
} catch(error) {
throw error;
}
}