Lo streaming della risposta in Express non funziona nel servizio app di Azure

Aug 24 2020

Sto provando a trasmettere le risposte al mio client utilizzando un server NodeJS Express ospitato tramite il servizio app di Azure. Tuttavia, ho notato che non è realmente in streaming ma cerca di inviare la risposta nel suo insieme. Quando la dimensione della risposta è enorme (> 50 MB), il client riceve un messaggio Internal Server Error, ma il server non genera un errore.

Inoltre, quando eseguo il server all'interno di un Docker (Node Image :) 10.22.0-alpine3.9, vedo che il client riceve la risposta come flusso anche per risposte enormi. (Questo è il comportamento di cui ho effettivamente bisogno)

Il mio web.configfile è il seguente.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="iisnode" path="server.js" verb="*" modules="iisnode" responseBufferLimit="0"/>
    </handlers>
    <iisnode flushResponse="true" />
    ...
  </system.webServer>
</configuration>

Questa è una piccola spiegazione di ciò che fa il mio programma.

Ho un'API esterna che restituisce un oggetto simile al seguente.

{
  "title":"Test Title",
  "lastBuildDate":"1597981114347",
  "items":[
    {
      id: 'item1',
      value: 'value1'
    },
    {
      id: 'item2',
      value: 'value2'
    },
    ...
  [
}

Voglio filtrare solo gli elementi itemsnell'array e inviarli al client. Il cliente dovrebbe ricevere una risposta come di seguito.

[
   {
     id: 'item1',
     value: 'value1'
   },
   {
     id: 'item2',
     value: 'value2'
   },
   ...
[

A volte questo oggetto è troppo grande (> 50 MB) e per questo motivo, invio la risposta come flusso per evitare di utilizzare troppa memoria buffer nel mio server. Di seguito è riportato il codice che ho utilizzato per lo streaming della risposta.

const https = require('https');
const { withParser } = require('stream-json/filters/Pick');
const { streamArray } = require('stream-json/streamers/StreamArray');
const { chain } = require('stream-chain');

exports.getStreamResponse = async function (req, res) {
  const options = {
    hostname,
    port,
    path,
    method: 'GET',
  };

  return new Promise((resolve, reject) => {
    https.request(options, (dataStream) => {
      const pipeline = chain([
        dataStream,
        withParser({ filter: 'items' }),
        streamArray()
      ]);
  
      res.write("[");
  
      let separator = '';
  
      pipeline.on('data', data => {
        res.write(separator + JSON.stringify(data.value));
        if (!separator) {
          separator = ',';
        }
      });
  
      pipeline.on('end', () => {
        res.write("]");
        res.end();
        resolve();
      });

      pipeline.on('error', (error) => {
        reject(error);
      });
    });
  })
};
            

Ho anche notato che se scrivo il codice come di seguito, ottengo sempre una risposta in streaming. Tuttavia, la risposta non è nel formato corretto come necessario.

https.request(options, (dataStream) => {
  dataStream.pipe(res);
});

Risposte

3 ErangaHeshan Aug 28 2020 at 16:48

Come ho descritto nell'ultima parte della mia domanda, convogliare direttamente res(la mia risposta al client) a dataStream(il flusso di dati che ho ricevuto dall'API esterna) ha permesso di eseguire lo streaming senza problemi.

Estendendo lo stesso comportamento, ho creato uno Readablestream che è equivalente alla risposta che dovrei inviare al mio cliente. Poi l'ho convogliato rese ha funzionato.

Ecco la mia soluzione.

const https = require('https');
const { withParser } = require('stream-json/filters/Pick');
const { streamArray } = require('stream-json/streamers/StreamArray');
const { chain } = require('stream-chain');
const { Readable } = require('stream');

exports.getStreamResponse = async function (req, res) {
  const options = {
    hostname,
    port,
    path,
    method: 'GET',
  };

  return new Promise((resolve, reject) => {
    https.request(options, (dataStream) => {
      const pipeline = chain([
        dataStream,
        withParser({ filter: 'items' }),
        streamArray()
      ]);
  
      // create a readable stream to collect data from response 
      const readable = new Readable({
        // this empty method is to avoid 'ERR_METHOD_NOT_IMPLEMENTED'
        // error when read method is called while there is no data in the
        // readable stream
        read(size) { }
      });
  
      let separator = '';
  
      readable.pipe(res);
      readable.push("[");

      pipeline.on('data', data => {
        readable.push(separator + JSON.stringify(data.value));
        if (!separator) {
          separator = ',';
        }
      });

      pipeline.on('end', () => {
        readable.push("]");
        readable.push(null);
        resolve();
      });
            
      pipeline.on('error', reject);
    });
  })
};

Tuttavia, ho notato che questa soluzione richiede più memoria rispetto alla soluzione con cui ho avuto problemi. Probabilmente perché sto creando un flusso leggibile che è ridondante.