Ошибка "Содержимое заголовка содержит недопустимые символы" при отправке части многоэтапной загрузки в новый запрос
Мой экспресс-сервер получает загрузки файлов из браузеров. Загрузки передаются как multipart/form-data
Запросы; Я использую многопартийность для разбора тела входящего объекта.
Многопартийность позволяет вам получить часть (примерно, одно поле формы, как <input type="file">
) как читаемый поток. Я не хочу обрабатывать или хранить загруженные файлы на моем веб-сервере, поэтому я просто передаю часть загруженного файла в запрос, направленный в другой сервис (используя модуль запроса).
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
заголовок и стек не содержат никакого моего кода.
Почему мои загрузки иногда терпят неудачу?
4 ответа
Тот TypeError
генерируется узлом при отправке исходящего HTTP-запроса, если в запросе есть какая-либо строка headers
Объект option содержит символ вне основного диапазона 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
объект, который также является читаемым потоком, представляющим тело детали. Когда ты pipe()
part
в 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...]
... за исключением того, что узел видит не-ASCII символ в Content-Disposition
заголовок и броски. Сгенерированная ошибка перехватывается запросом и передается функции обратного вызова запроса как err
,
Этого поведения можно избежать, удалив заголовки деталей перед передачей их в запрос.
delete part.headers;
part.pipe(serviceRequest);
В этом примере показано, как отправить файл в виде вложения с национальными символами в имени файла.
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) => ...);
Вы можете использовать серверную часть 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;
}
}
Как и раньше, @arrow cmt, используя encodeURI(имя файла) в заголовке Content-disposition. В клиенте вы используете метод decodeURI для декодирования.