Lo streaming della risposta in Express non funziona nel servizio app di Azure
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.config
file è 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 items
nell'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
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 Readable
stream che è equivalente alla risposta che dovrei inviare al mio cliente. Poi l'ho convogliato res
e 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.