cloudflare / cloudflare/workerd

Same-socket pipeTo echo over a service binding deadlocks a client that awaits write() before reading

Open
#7,314 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
8.7k
Forks
739
Avg merge
2d 20h
Merged PRs (30d)
174

Description

Over a service binding (`env.X.connect()`, and likewise a Durable Object stub), a `connect()` handler that echoes with `socket.readable.pipeTo(socket.writable)` deadlocks a client that awaits `write()` before reading: the client's `write()` never resolves. The same handler written as an explicit `reader.read()` / `writer.write()` loop echoes correctly.

The connection between the two Workers is an in-process kj two-way pipe with no buffer, and `pipeTo` between the socket's own halves becomes a zero-copy pump. The client's write therefore only completes once the pumped bytes are written into the client's readable, which the client only reads after its write resolves. A TCP connection has kernel buffers on both sides, so the same client and server work over a `sockets` TCP listener.

## Repro

```js
// worker.js
export default {
async connect(socket) {
await socket.readable.pipeTo(socket.writable);
},
};

export const echoRoundTrip = {
async test(ctrl, env) {
const socket = env.SELF.connect('echo:1');
const writer = socket.writable.getWriter();
const reader = socket.readable.getReader();
const timer = setTimeout(() => console.log('write() still pending after 2s'), 2000);
await writer.write(new TextEncoder().encode('ping'));
clearTimeout(timer);
const { value } = await reader.read();
console.log('echoed:', new TextDecoder().decode(value));
},
};
```

```capnp
using Workerd = import "/workerd/workerd.capnp";
const unitTests :Workerd.Config = (
services = [( name = "main", worker = (
modules = [(name = "worker", esModule = embed "worker.js")],
compatibilityDate = "2025-06-01",
bindings = [(name = "SELF", service = "main")],
))],
);
```

`workerd test worker.wd-test` prints `write() still pending after 2s` and the test times out. Replacing the handler body with

```js
const reader = socket.readable.getReader();
const writer = socket.writable.getWriter();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
await writer.write(value);
}
await writer.close();
```

prints `echoed: ping` and passes. Observed on `main` at 925464b.

## Impact

- `socket.readable.pipeTo(socket.writable)` is the canonical echo, and "write the request, await it, then read the response" is the normal shape of a request/response client, so this is easy to hit in tests and in Worker-to-Worker or Worker-to-Durable-Object socket protocols.
- The failure is a silent hang with no error on either side.
- It only arises when the response path leads back to the caller without a buffer in between: same-socket pipes and loopback shapes. A handler piping to a real upstream (a `sockets` TCP connection) is unaffected because the upstream buffers.
- Programs ported from Node.js or POSIX assume socket-buffer semantics; there is no equivalent divergence over real TCP.

## Possible resolutions

A bounded buffer in the in-process two-way pipe so that a `write()` completes on enqueue as it does into a kernel socket buffer, or documenting that `pipeTo` echo over service bindings requires the client to read concurrently with writing.

Found while testing `net.Server` over `connect()` (#7306); the `net.Socket` adapter uses an explicit read/write loop and is not affected.

Contributor guide

Open the contributing guide

Research direction

Run `workerd test worker.wd-test` with the `worker.js` reproduction, then trace the in-process kj two-way pipe used by `env.X.connect()` and the socket `pipeTo` path. Done means the existing client can await `writer.write()` before reading, receives `ping`, and the test no longer hangs; compare with the explicit reader/writer loop and the TCP listener behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, javascript
Domain
backend, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.