Skip to content

Channel.decodeText corrupts UTF-8 across chunk boundaries #2194

Description

@skoshx

What version of Effect is running?

effect@4.0.0-beta.66

What steps can reproduce the bug?

Single file, single dependency. Crafts one JSONL line where an em-dash (e2 80 94) straddles byte 65535/65536 — the default
fs.stream chunk boundary — then runs it through Ndjson.decodeSchema.

// repro.mjs
import { Effect, FileSystem, Schema, Stream } from "effect"
import * as Ndjson from "effect/unstable/encoding/Ndjson"
import * as NodeServices from "@effect/platform-node/NodeServices"
import { writeFileSync } from "node:fs"

const filler = "x".repeat(65526)
const line = JSON.stringify({ msg: `${filler}—end` }) + "\n"
writeFileSync("/tmp/repro.jsonl", line, "utf8")

const Row = Schema.Struct({ msg: Schema.String })

const program = Effect.gen(function* () {
  const fs = yield* FileSystem.FileSystem
  const rows = yield* fs.stream("/tmp/repro.jsonl").pipe(
    Stream.pipeThroughChannel(
      Ndjson.decodeSchema(Row)({ ignoreEmptyLines: true }),
    ),
    Stream.runCollect,
  )
  const tail = rows[0].msg.slice(-10)
  console.log("tail (chars):", JSON.stringify(tail))
  console.log("tail (bytes):", Buffer.from(tail, "utf8").toString("hex"))
  console.log("contains em-dash:", tail.includes("\u2014"))
})

await Effect.runPromise(program.pipe(Effect.provide(NodeServices.layer)))
{
  "type": "module",
  "dependencies": {
    "@effect/platform-node": "4.0.0-beta.66",
    "effect": "4.0.0-beta.66"
  }
}
pnpm i && node repro.mjs

Expected

tail (chars): "xxxxx—end"
tail (bytes): 7878787878e28094656e64
contains em-dash: true

Actual

tail (chars): "xxxxx��end"
tail (bytes): 7878787878efbfbdefbfbd656e64
contains em-dash: false

What is the expected behavior?

Expected

tail (chars): "xxxxx—end"
tail (bytes): 7878787878e28094656e64
contains em-dash: true

What do you see instead?

Actual

tail (chars): "xxxxx��end"
tail (bytes): 7878787878efbfbdefbfbd656e64
contains em-dash: false

Additional information

a similar bug was fixed in the Stream module, but looks like Channel.decodeText was missed

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions