Open7

Node.jsでStreamを読むとき、chunkを文字列として結合してはいけない

kazuma1989kazuma1989

次のBADパターンをやってはいけないということ。resultが期待どおりにならない場合があるため。

BAD
let result = ""

for await (const chunk of stream) {
  result += chunk
}
GOOD
const chunks = []

for await (const chunk of stream) {
  chunks.push(chunk)
}

const result = Buffer.concat(chunks).toString()
kazuma1989kazuma1989

result += chunkは、暗黙的にchunk.toString()を呼んでいるが、chunkがテキストとして区切りのいいバイナリデータとなっている保証がないため。

kazuma1989kazuma1989

ちなみに for await (const chunk of stream) { ... }stream.on("data", (chunk) => { ... }) と同じ。

kazuma1989kazuma1989

実験してみる。

text
SUCCESS🎉
bad.js
import * as fs from "node:fs"

const stream = fs.createReadStream("./text", { highWaterMark: 4 })

let result = ""

for await (const chunk of stream) {
  result += chunk
}

console.log(result)
good.js
import * as fs from "node:fs"

const stream = fs.createReadStream("./text", { highWaterMark: 4 })

const chunks = []

for await (const chunk of stream) {
  chunks.push(chunk)
}

const result = Buffer.concat(chunks).toString()

console.log(result)
$ node bad.js 
SUCCESS����
$ node good.js 
SUCCESS🎉
kazuma1989kazuma1989

textファイルの中身をバイナリとして見ると、絵文字部分が4 byte f0 9f 8e 89 で構成されているのがわかる。

$ od -tx1 text 
0000000    53  55  43  43  45  53  53  f0  9f  8e  89                    
0000013

fs.createReadStreamhighWaterMark: 4を指定して、4 byteごとにこのファイルを区切っていくと、絵文字が絵文字の塊にならない。

53  55  43  43    SUCC
45  53  53  f0    ESS�
9f  8e  89        ���
kazuma1989kazuma1989

highWaterMark: 7にすると、bad.jsでも期待どおりの出力となるが、これはたまたま区切りがいい位置にきただけのこと。
一般性はないので注意。

53  55  43  43  45  53  53    SUCCESS
f0  9f  8e  89                🎉