Reading from a socket gets stuck on Node.js 26
Maintainers usually reply within 3 days
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 72/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- node.js, scala
- Domain
- networking
Research direction
Start with the reproduction test described for IoPlatformSuite, then trace the Scala.js readable-stream code that calls read() on Node's 'readable' event. Add a regression test for buffered chunks and verify it reads all data on Node.js 26 while remaining compatible with older Node.js versions.
Written by the indexing model from the issue text.
Description
On Node.js 26, socket.reads can stop returning data even though the data has arrived. It works on Node.js 25 and earlier.
- fs2-io 3.14.0, also on
main(419d575) - cats-effect 3.7.1
- Scala 2.13 and 3.3.7
- Scala.js 1.22.0, Node.js 26.13.0, macOS
- JVM and Native are fine
How to reproduce
This test, added to IoPlatformSuite, puts three pieces of data in a stream and reads them. On Node.js 26 it times out after getting only the first piece:
test("reads every chunk already buffered in a Readable") {
def readable = {
val duplex = new facade.stream.Duplex(
new facade.stream.DuplexOptions {
var autoDestroy = false
var read = _ => ()
var write = (_, _, _, _) => ()
var `final` = (_, _) => ()
var destroy = (_, _, _) => ()
}
)
List[Byte](0, 1, 2).foreach(b => duplex.push(Chunk(b, b).toUint8Array))
duplex
}
Stream
.resource(suspendReadableAndRead[IO, Duplex]()(readable))
.flatMap(_._2)
.take(6)
.compile
.toList
.timeout(1.second)
.assertEquals(List[Byte](0, 0, 1, 1, 2, 2))
}
You can also see it with a real socket:
//> using scala 3.3.7
//> using platform scala-js
//> using jsVersion 1.22.0
//> using jsModuleKind common
//> using dep co.fs2::fs2-io::3.14.0
import cats.effect.{IO, IOApp, Resource}
import com.comcast.ip4s.*
import fs2.io.net.Network
import scala.concurrent.duration.*
import scala.scalajs.js
object Main extends IOApp.Simple {
private val net = js.Dynamic.global.require("net")
// sends 8 bytes three times, 50ms apart, then stays quiet
private val server: Resource[IO, Int] =
Resource.make(IO.async_[js.Dynamic] { cb =>
val s = net.createServer { (socket: js.Dynamic) =>
socket.write("aaaaaaaa")
js.timers.setTimeout(50)(socket.write("bbbbbbbb"))
js.timers.setTimeout(100)(socket.write("cccccccc"))
()
}
s.listen(0, "127.0.0.1", () => cb(Right(s)))
})(s => IO(s.close()).void).map(_.address().port.asInstanceOf[Int])
val run: IO[Unit] =
server.use { port =>
Network[IO].connect(SocketAddress(ipv4"127.0.0.1", Port.fromInt(port).get)).use { socket =>
// wait until all 24 bytes have arrived, then read them
IO.sleep(500.millis) >>
socket.reads.chunks
.evalTap(chunk => IO.println(s" got ${chunk.size} bytes"))
.flatMap(fs2.Stream.chunk)
.take(24)
.compile
.count
.timeout(3.seconds)
.attempt
.flatMap {
case Right(n) => IO.println(s"Node ${js.Dynamic.global.process.version}: read all $n bytes")
case Left(_) => IO.println(s"Node ${js.Dynamic.global.process.version}: got stuck, the rest never came")
}
}
}
}
On Node.js 26:
got 8 bytes
got 8 bytes
Node v26.13.0: got stuck, the rest never came
On Node.js 25 (same on 24 and 22):
got 8 bytes
got 16 bytes
Node v25.9.0: read all 24 bytes
Why
Node.js 26 changed readable.read(): it now returns one piece of the buffered data at a time, where it used to return all of it (nodejs/node#60441).
fs2 calls read() once each time Node fires 'readable'. Node only fires 'readable' again once everything has been read or more data comes in. So on Node.js 26, if nothing else comes in, the rest of the data is never read.
Fix
Keep calling read() until it returns null. On older Node.js versions the first call already returns everything, so nothing changes. PR to follow.
- Dominant language
- Scala
- Stars
- 2.5k
- Forks
- 633
- Avg merge
- 4d 16h
- Merged PRs (30d)
- 5
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
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 typelevel/fs2
-
Unix socket bind may delete an existing file when binding failsPossibly taken @abhinav-phi claimed this 3 days ago. Openbug
Difficulty 3/5 1-2 days Newbie friendliness 55/100
Maintainers usually reply within 3 days
-
Scala Native TLSSocket#applicationProtocol behavior is inconsistent with JVMPossibly taken @reardonj claimed this 19 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 25/100
Maintainers usually reply within 3 days
-
Feature Proposal: report progress writing to socketsPossibly taken @reardonj claimed this 26 days ago. Open
Difficulty 5/5 Over a week Newbie friendliness 45/100
Maintainers usually reply within 3 days
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
typelevel/fs2#3719 · 2 comments ·
Maintainers usually reply within 3 days
-
Difficulty 3/5 1-2 days Newbie friendliness 55/100
Maintainers usually reply within 3 days
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
propensive/soundness#2351 ·
Maintainers usually reply within 1 day
-
Traffic event delivered/rejected counters appear swapped in TrafficStateController.updateWithReceiptOpen
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
digital-asset/canton#705 ·
Maintainers usually reply within 2 days
-
easy first-contributor first-contributors good first issue help wanted scala up-for-grabs
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
fork-commit-merge/fork-commit-merge#8457 ·
Maintainers usually reply within 1 day
-
[GC] Empty or non-numeric GC_*_HEAP_SIZE reads out of bounds or uses an uninitialised sizePossibly taken @oluies claimed this 2 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
scala-native/scala-native#5177 ·
Maintainers usually reply within 1 day
-
good first issue
Difficulty 1/5 Under an hour Newbie friendliness 78/100
promovolve/promovolve#358 ·
Maintainers usually reply within 1 day