[Node.js] pipeを使うときに気をつけること

はじめに

Node.jsのストリームは大容量のファイルやBlobをメモリ面で効率よく使いたいときにとても便利です。Streamは内部に固定長のバッファを持つことによって、バッファがあふれないようににデータの読み込み/書き込みを調整してくれます。

また、pipe()メソッドを使うことによってストリーム通しを繋げてパイプラインを簡単に作ることができます。簡単に作ることはできるのですが挙動を把握しておかないと、プロセスがくらっしゅしてしまったりメモリリークの原因となってしまうので注意が必要です。

今回はpipe()メソッドを使うときの注意点をtipsとしてまとめます。

pipeをつかったときのストリームの挙動

サンプルとして、「サーバにHTTP接続すると、サーバ上にあるファイルをダウンロードできる」というものを用意してみました。HTTPサーバはhttpモジュールを使います。レスポンスであるServerResponseはwritableストリームなので、fsモジュールでファイルのreadableストリームを作り、これらをpipe()メソッドで繋げます。

import http from 'http'
import fs from 'fs'

const filename = process.argv[2]

const server = http.createServer((req, res) => {
    const stream = fs.createReadStream(filename)
    stream.pipe(res)
})

server.listen(3000, () => console.log(`http://localhost:3000`))

正常系についてはこれだけでも十分に動作します。しかし異常系についてはプロセスがクラッシュしてしまいます。

というのも、StreamEventEmitterを継承しています。そしてEventEmitterにおいて、errorイベントに対するリスナーが登録されていない状態でerrorイベントが発生した場合、Node.jsはスタックトレースを出力してプロセスを終了させてしてしまいます。試しにreadable.destryo()を使って、Readableストリームでerrorイベントを発生させてみます。

const server = http.createServer((req, res) => {
    const stream = fs.createReadStream(filename)
    stream.pipe(res)
    stream.destroy(new Error('エラーを意図的に起こす'))
})

// サーバにHTTP接続すると、以下のスタックトレースを吐き出してプロセスが終了する
// Error: エラーを意図的に起こす
//     at Server.<anonymous> 
//     at Server.emit (events.js:210:5)
//     at parserOnIncoming (_http_server.js:745:12)
//     at HTTPParser.parserOnHeadersComplete (_http_common.js:115:17)
// [ERROR] 15:30:55 Error: エラーを意図的に起こす

プロセスが死にました。このままではサーバの役割を果たせないので、Readableストリームにエラーイベントに対するリスナーを追加します。

const server = http.createServer((req, res) => {
    const stream = fs.createReadStream(filename)
    stream.on('error', (error) => {
        console.log(`${error}`)
        // some error handling...
    })
    stream.pipe(res)
    stream.destroy(new Error('エラーを意図的に起こす'))
})

これでReadableストリームでエラーが発生してもプロセスが落ちることはなくなりました。しかしまだ問題があります。ブラウザからこのサーバに接続すると、サーバ側ではエラーイベントをハンドリングはするのですが、ブラウザから見るとレスポンスがいつまでたっても返ってきません。

この事象が発生する原因はドキュメントに書かれていますpipe()を使ってストリームを繋げたとき、Redableストリーム側で読み込みが終わった、すなわちendイベントがemitされると接続されているwritableストリームのend()をオートでコールしてくれます。ただし、errorイベントが発生した場合はその限りではなく、writableストリームは開きっぱなしなので手動で閉じてあげる必要があります。

const server = http.createServer((req, res) => {
    const stream = fs.createReadStream(filename)
    stream.on('error', (error) => {
        console.log(`${error}`)
        res.statusCode = 500
        res.end('500 error')
    })
    stream.pipe(res)
    stream.destroy(new Error('エラーを意図的に起こす'))
})

これでエラー発生時に500エラーをブラウザに返してくれます。

ストリームはとても便利なものですが、エラーハンドリングを疎かにするとアプリのクラッシュに繋がるので注意しましょう。

Profile
d_yama
元Microsoft MVP for Windows Development(2018-2020)
Sub-category : Windows Mixed Reality
Search