DioxusLabs / DioxusLabs/dioxus

`dx serve` only replays hot-reload backlog to the first WS client when stdout is not a TTY

Open
#5,505 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Rust
Stars
39.1k
Forks
1.9k
Avg merge
4d 10h
Merged PRs (30d)
4

Description

# `dx serve` only replays hot-reload backlog to the first WS client when stdout is not a TTY

## Environment

- `dx` CLI: 0.7.6 (installed via `cargo install dioxus-cli --version 0.7.6 --locked`)
- `dioxus` crate: 0.7
- Platform: macOS 14 / Darwin 24.6.0, x86_64 and aarch64 both reproduce
- Target: `--platform web` (WASM)

## Summary

When `dx serve` is run without a controlling TTY (stdout piped/redirected, as happens in any orchestrator or shell script that runs dx in the background), the hot-reload "backlog replay on new WS client" behaviour breaks after the first connection. Fresh browser refreshes see the initial on-disk build forever, even though dx is logging `Hotreloading: ...` and live-connected clients are still receiving patches correctly.

Wrapping the same command in a PTY (e.g. BSD `script -q /dev/null dx serve ...`) makes every fresh WS client receive the backlog as expected.

## Reproduction

Minimal Dioxus 0.7 web crate with any RSX.

### Broken case (no TTY)

```bash
dx serve --platform web --addr 127.0.0.1 --port 8787 --open false > /tmp/dx.log 2>&1 &
# (optional) --interactive true <-- does not help; dx checks is_terminal()
```

1. Wait for `Build completed successfully`.
2. Edit `src/main.rs` in-place (preserving inode) — change any RSX text.
3. Observe `dx` log: `Hotreloading: /src/main.rs` ✅.
4. Connect a fresh WS client to `ws://127.0.0.1:8787/_dioxus`. **First** connect receives 1 message (~7.5 KB) containing the templates with the edit. ✅
5. Edit `src/main.rs` again. Log: `Hotreloading: /src/main.rs` again.
6. Connect 5 more fresh WS clients, each with a 1.5 s receive window. **All 5 receive zero bytes.** ❌
7. Any further source edits reproduce step 6 — the backlog never serves to subsequent fresh clients for the lifetime of this `dx serve`.

### Working case (PTY)

```bash
script -q /dev/null dx serve --platform web --addr 127.0.0.1 --port 8787 --open false
```

Identical steps. All 5 sequential fresh WS clients receive the ~7.5 KB backlog with the latest edit. ✅

### Minimal WS test harness (Node, uses repo-local `ws@8`)

```js
const WebSocket = require('ws');
for (let i = 1; i <= 5; i++) {
await new Promise((resolve) => {
const ws = new WebSocket('ws://127.0.0.1:8787/_dioxus');
let bytes = 0;
ws.on('message', (d) => bytes += d.length);
setTimeout(() => { console.log(`client ${i}: ${bytes} bytes`); ws.close(); resolve(); }, 1500);
});
await new Promise(r => setTimeout(r, 300));
}
```

Non-TTY output:
```
client 1: 7574 bytes # only AFTER the very first edit, before any client has connected
client 2: 0 bytes
client 3: 0 bytes
client 4: 0 bytes
client 5: 0 bytes
```

PTY output (`script -q /dev/null ...`):
```
client 1: 7560 bytes
client 2: 37800 bytes # received live patch mid-window
client 3: 7560 bytes
client 4: 7560 bytes
client 5: 7560 bytes
```

## Expected behaviour

Per `packages/cli/src/serve/mod.rs:106-119`, `ServeUpdate::NewConnection` fires unconditionally for every new WS client and `devserver.send_hotreload(builder.applied_hot_reload_changes(BuildId::PRIMARY))` should replay the cumulative backlog. This is documented in-code at `packages/cli/src/build/builder.rs` around the `patches` field: *"the cumulative history of patches applied to this run of the app — used so that a fresh client connecting mid-session can be brought up to date"*.

Every fresh WS client should receive the merged templates / assets / last jump_table regardless of whether stdout is a TTY.

## What I investigated

Reading `packages/cli/src/serve/**` at v0.7.6:

- `applied_hot_reload_changes()` (`runner.rs:835-854`) `.clone()`s the aggregate — it does not drain.
- `add_hot_reload_message()` (`runner.rs:920-938`) merges into `applied_client_hot_reload_message` and is reached from the `Hotreloading` branch at `runner.rs:534-577`. The log line proves this path runs.
- `clear_hot_reload_changes()` is called only on full-rebuild / hotpatch paths (`runner.rs:521, 530, 743`). None are gated on `interactive` / TTY. We never pass `--hot-patch`.
- The only TTY-gated logic in the CLI is `is_interactive_tty()` at `cli/serve.rs:107-110`:
```rust
pub(crate) fn is_interactive_tty(&self) -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal() && self.interactive.unwrap_or(true)
}
```
This value only reaches `open_browser` (`runner.rs:126`), TUI rendering (`output.rs`), and tracer redirection (`runner.rs:98`). It is not read anywhere in the backlog, replay, or send paths.
- `server.rs:275-278` short-circuits `send_hotreload` when `reload.is_empty()`, but the clone returned by `applied_hot_reload_changes()` should not be empty after a successful `add_hot_reload_message` call.

So from a static-analysis standpoint the backlog should be populated and replay should fire. The TTY→replay-breaks correlation suggests a runtime-level issue — maybe stdout write back-pressure stalling the async task that services `WebServer::send_hotreload`, maybe a `tracing` layer buffering differently when stdout is a pipe, or an interaction between `--interactive=false` implicit defaults and the async runtime. I couldn't pin the exact cause without instrumented builds.

## Workaround

Wrap dx in a PTY. `script -q /dev/null dx serve ...` (BSD `script`, macOS) or `script -q -c "dx serve ..." /dev/null` (util-linux). Fixes the replay for every reconnect and requires no source changes.

## Why this matters

Any orchestrator that runs `dx serve` under a pipe (a shell script launched from `beforeDevCommand` in Tauri, a CI smoke test, a `concurrently`-style multiplexer) silently loses hot-reload across refreshes. The app appears to "revert to the initial build" on every Cmd+R, which looks like a caching bug until you trace it through WS frames.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.