Expressでの応答ストリーミングはAzureAppServiceでは機能しません

Aug 24 2020

Azure AppServiceを使用してホストされているNodeJSExpressサーバーを使用して、クライアントに応答をストリーミングしようとしています。しかし、実際にはストリーミングではなく、全体として応答を送信しようとしていることに気付きました。応答サイズが大きい場合(> 50MB)、クライアントはを受け取りInternal Server Errorますが、サーバーはエラーをスローしません。

さらに、Docker内でサーバーを実行すると(ノードイメージ:) 10.22.0-alpine3.9、クライアントは巨大な応答に対してもストリームとして応答を取得することがわかります。(これは私が実際に必要とする動作です)

私のweb.configファイルは以下の通りです。

<?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>

これは私のプログラムが何をするかについての小さな説明です。

次のようなオブジェクトを返す外部APIがあります。

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

items配列内の要素のみをフィルタリングして、それらをクライアントに送信したいと思います。クライアントは次のような応答を受け取るはずです。

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

このオブジェクトが大きすぎる(> 50MB)場合があるため、サーバーでバッファメモリを使いすぎないように、応答をストリームとして送信しています。以下は、応答をストリーミングするために使用したコードです。

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);
      });
    });
  })
};
            

また、以下のようなコードを書くと、常にストリーム応答が返されることに気づきました。ただし、応答は必要に応じて正しい形式ではありません。

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

回答

3 ErangaHeshan Aug 28 2020 at 16:48

質問の後半で説明したように、res(クライアントへの応答)をdataStream(外部APIから取得したデータストリーム)に直接パイプすることで、問題なくストリーミングできるようになりました。

同じ動作を拡張しReadableて、クライアントに送信する必要のある応答と同等のストリームを作成しました。それから私はそれをパイプでつなぎました、resそしてそれは働きました。

これが私の解決策です。

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);
    });
  })
};

ただし、このソリューションには、問題が発生したソリューションよりも多くのメモリが必要であることに気付きました。おそらく、冗長な読み取り可能なストリームを作成しているためです。