Ошибка "Содержимое заголовка содержит недопустимые символы" при отправке части многоэтапной загрузки в новый запрос

Мой экспресс-сервер получает загрузки файлов из браузеров. Загрузки передаются как 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 для декодирования.

Другие вопросы по тегам