Socket.readerString corrupts UTF-8 characters split across TCP chunks
Maintainers usually reply within 1 day
A pull request for this has already been merged.
- #9487 by @jayhemnani9910 — merged
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- nodejs, typescript
- Domain
- networking
Research direction
Start with the reproduction in the issue and inspect packages/effect/src/socket/Socket.ts:268, where Socket.readerString decodes each item independently. Determine whether streaming decoding should apply only to byte-stream transports or also to WebSocket binary messages, especially for BOM handling, truncated input, and normal close. Add regression tests covering UTF-8 characters split across TCP chunks and the agreed WebSocket behavior.
Written by the indexing model from the issue text.
Description
What version of Effect is running?
effect 4.0.5, @effect/platform-node 4.0.5 (same code on main 7f32606ba)
What steps can reproduce the bug?
A TCP server sends café ☕ done one byte at a time. The client reads it with Socket.readerString over NodeSocket.makeNet:
import * as net from "node:net"
import { Effect } from "effect"
import * as Socket from "effect/socket/Socket"
import * as NodeSocket from "@effect/platform-node/NodeSocket"
const bytes = Buffer.from("café ☕ done", "utf8")
const server = net.createServer(async (c) => {
for (let i = 0; i < bytes.length; i++) {
c.write(bytes.subarray(i, i + 1))
await new Promise((r) => setTimeout(r, 5))
}
c.end()
})
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r))
const { port } = server.address() as net.AddressInfo
const text = await Effect.gen(function*() {
const socket = yield* NodeSocket.makeNet({ host: "127.0.0.1", port })
const pull = yield* Socket.readerString(socket)
let text = ""
while (true) {
const r = yield* Effect.result(pull)
if (r._tag === "Failure") return text
text += r.success.join("")
}
}).pipe(Effect.scoped, Effect.runPromise)
console.log(JSON.stringify(text))
server.close()
What is the expected behavior?
"café ☕ done"
What do you see instead?
"caf�� ��� done"
Additional information
readerString creates one TextDecoder per acquisition but calls decoder.decode(item) without { stream: true } (packages/effect/src/socket/Socket.ts:268), so every chunk is decoded on its own. TCP chunk boundaries are arbitrary, so any multi-byte character can land on one. toChannelString uses the same reader, and DevTools runs NDJSON over it.
Decoding in streaming mode fixes TCP, but it changes WebSocket binary messages, which are complete on their own:
- A UTF-8 BOM is stripped from every message today; in streaming mode only from the first. Two binary messages
EF BB BF 7B 7Dgive["{}", "{}"]today and["{}", "{}"]in streaming mode. - A truncated message's leftover bytes would join the next message instead of becoming U+FFFD.
I have a fix that decodes in streaming mode and, like #9436 for Channel.decodeText, flushes incomplete trailing bytes as U+FFFD on a normal close (code 1000), with tests. Is that the behaviour you want for WebSocket too, or should streaming decode apply only to byte-stream transports? Happy to open a PR either way.
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 808
- Avg merge
- 10h 22m
- Merged PRs (30d)
- 771
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Effect-TS/effect
-
BrowserWorkerRunner: port finalizer throws when the worker global has no close() (Bun)Possibly taken @santiago-ramos-02 claimed this 19 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Effect-TS/effect#8635 · 3 comments ·
Maintainers usually reply within 1 day
-
Support {self: this} for fnUntracedMay be free again @ArjunCodess claimed this 30 days ago, and no pull request is open. Openenhancement
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Effect-TS/effect#8101 · 1 comment ·
Maintainers usually reply within 1 day
-
HashMap.set keeps the old value when the new one is Equal to itPossibly taken @schutzelaars claimed this today. Open
Difficulty 3/5 1-2 days Newbie friendliness 58/100
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 74/100
Maintainers usually reply within 1 day
-
OpenApi: optional query and header parameters are documented as nullablePossibly taken @Marve10s claimed this 2 days ago. Open
Difficulty 4/5 3-5 days Newbie friendliness 57/100
Maintainers usually reply within 1 day
All issues in Effect-TS/effect
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
simkimsia/netlify-axi#19 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
subhranshudash13-dotcom/Contribo#74 ·
Maintainers usually reply within 1 day
-
enhancement frontend good first issue Stellar Wave trivial
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Godgift-care/sci-healthcare-frontend#166 · 1 comment ·
Maintainers usually reply within 1 day
-
backend good first issue security Stellar Wave trivial
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Godgift-care/sci-healthcare-backend#171 · 1 comment ·
Maintainers usually reply within 1 day