[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`))
正常系についてはこれだけでも十分に動作します。しかし異常系についてはプロセスがクラッシュしてしまいます。
というのも、Stream
はEventEmitter
を継承しています。そして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エラーをブラウザに返してくれます。
ストリームはとても便利なものですが、エラーハンドリングを疎かにするとアプリのクラッシュに繋がるので注意しましょう。