Skip to content

fix(ping): reject when the connection closes before a status response - #1514

Merged
rom1504 merged 1 commit into
masterfrom
fix-ping-closed-connection
Aug 27, 2026
Merged

fix(ping): reject when the connection closes before a status response#1514
rom1504 merged 1 commit into
masterfrom
fix-ping-closed-connection

Conversation

@u9g

@u9g u9g commented Aug 22, 2026

Copy link
Copy Markdown
Member

Problem

ping() settles on exactly three things: server_info, ping, or a client error. A server that accepts the connection and then closes it without answering is invisible to all three — the promise sits until closeTimeout (120 s by default) and then rejects with ETIMEDOUT, blaming a timeout that never actually happened, long after the server already told us it wasn't going to answer.

This isn't hypothetical: vanilla servers do it on every startup. A status request is closed without a reply — no packet, just a FIN — until the server has built the status snapshot it serves, and that doesn't happen until startup has fully finished, slightly after the Done (…)! line is logged.

So pinging a server the moment it reports being up can land in that gap. Probing a real server every 25 ms across a boot: 26.1 closes status connections until 52 ms after the Done line and first answers at 77 ms (it saves every dimension in between), while 1.21.8 is already answering 11 ms before it.

This surfaces downstream as mineflayer's external tests intermittently failing on 26.1 with a bare mocha hook timeout (PrismarineJS/mineflayer#3976).

Fix

Reject on end when no status has arrived yet:

client.on('end', function () {
  if (gotStatus) return
  clearTimeout(closeTimer)
  reject(new Error('Connection closed before the server sent a status response'))
})

If the status did arrive and the server closes before the pong, the existing noPongTimeout path already resolves with the status — that case is deliberately left alone, so this is only ever converting a 120 s misleading timeout into an immediate, accurate error.

Test

test/pingTest.js points a ping at a socket server that accepts the handshake and hangs up. Without the fix it fails after the full closeTimeout with ETIMEDOUT; with it, it fails in ~400 ms with a message that says what happened.

✔ fails as soon as the server closes the connection without answering (408ms)

u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 22, 2026
…wers

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. `MinecraftServer.runServer()` only
builds its status snapshot after `initServer()` returns, and until that
snapshot exists `ServerHandshakePacketListenerImpl` closes status
connections without replying. 26.1 widened that window: its `initServer()`
logs `Done (...)!` and then calls `saveEverything(false, true, true)`, a full
save of every dimension, before returning — 1.21.8's returns right after the
log. That is the `Saving chunks for level ...` line only 26.1 prints after
`Done`. Since minecraft-wrap calls back on the `Done` line, we ping inside
the window: probing every 25ms across a boot, 26.1 hangs up on status until
52ms after `Done` and first answers at 77ms, while 1.21.8 already answers
11ms before it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's `saveEverything` call and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.
@u9g
u9g force-pushed the fix-ping-closed-connection branch from c261f43 to 79bae6f Compare August 22, 2026 19:53
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 22, 2026
…wers

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. `MinecraftServer.runServer()` only
builds its status snapshot after `initServer()` returns, and until that
snapshot exists `ServerHandshakePacketListenerImpl` closes status
connections without replying. 26.1 widened that window: its `initServer()`
logs `Done (...)!` and then calls `saveEverything(false, true, true)`, a full
save of every dimension, before returning — 1.21.8's returns right after the
log. That is the `Saving chunks for level ...` line only 26.1 prints after
`Done`. Since minecraft-wrap calls back on the `Done` line, we ping inside
the window: probing every 25ms across a boot, 26.1 hangs up on status until
52ms after `Done` and first answers at 77ms, while 1.21.8 already answers
11ms before it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's `saveEverything` call and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.
ping() settles on exactly three things: server_info, ping, or a client
error. A server that accepts the connection and then closes it without
answering is invisible to all three, so the promise sits until closeTimeout
(120s by default) and then rejects with ETIMEDOUT — a timeout that never
actually happened, reported long after the server already told us.

Vanilla servers do this on every startup: a status request is closed without
a reply until the server has built the status snapshot it serves, which does
not happen until startup has finished — slightly after the "Done" line is
logged. Probing a server every 25ms across a boot, 26.1 closes status
connections until 52ms past that line and first answers at 77ms, while
1.21.8 already answers 11ms before it. So pinging a server as soon as it
reports being up lands in that gap often enough to matter.

Reject on 'end' when no status has arrived yet. If the status did arrive and
the server closes before the pong, the existing noPongTimeout path already
resolves with it, so that case is left alone.
@u9g
u9g force-pushed the fix-ping-closed-connection branch from 79bae6f to 66bcc35 Compare August 22, 2026 19:58
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 22, 2026
…wers

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. A server closes status connections
without replying until it has built the status snapshot it serves, which
does not happen until startup has fully finished — slightly after the "Done"
line is logged. 26.1 widened that gap: it saves every dimension after
logging "Done" and before finishing startup, which is the
`Saving chunks for level ...` line only 26.1 prints after `Done`. Since
minecraft-wrap calls back on that line, we ping inside the gap: probing
every 25ms across a boot, 26.1 closes status connections until 52ms past
`Done` and first answers at 77ms, while 1.21.8 already answers 11ms before
it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's startup sequence and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.

@FloorIsGround FloorIsGround left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@rom1504
rom1504 merged commit 0903e59 into master Aug 27, 2026
35 checks passed
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…wers

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. A server closes status connections
without replying until it has built the status snapshot it serves, which
does not happen until startup has fully finished — slightly after the "Done"
line is logged. 26.1 widened that gap: it saves every dimension after
logging "Done" and before finishing startup, which is the
`Saving chunks for level ...` line only 26.1 prints after `Done`. Since
minecraft-wrap calls back on that line, we ping inside the gap: probing
every 25ms across a boot, 26.1 closes status connections until 52ms past
`Done` and first answers at 77ms, while 1.21.8 already answers 11ms before
it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's startup sequence and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…wers

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. A server closes status connections
without replying until it has built the status snapshot it serves, which
does not happen until startup has fully finished — slightly after the "Done"
line is logged. 26.1 widened that gap: it saves every dimension after
logging "Done" and before finishing startup, which is the
`Saving chunks for level ...` line only 26.1 prints after `Done`. Since
minecraft-wrap calls back on that line, we ping inside the gap: probing
every 25ms across a boot, 26.1 closes status connections until 52ms past
`Done` and first answers at 77ms, while 1.21.8 already answers 11ms before
it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's startup sequence and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…wers

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. A server closes status connections
without replying until it has built the status snapshot it serves, which
does not happen until startup has fully finished — slightly after the "Done"
line is logged. 26.1 widened that gap: it saves every dimension after
logging "Done" and before finishing startup, which is the
`Saving chunks for level ...` line only 26.1 prints after `Done`. Since
minecraft-wrap calls back on that line, we ping inside the gap: probing
every 25ms across a boot, 26.1 closes status connections until 52ms past
`Done` and first answers at 77ms, while 1.21.8 already answers 11ms before
it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's startup sequence and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…wers

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. A server closes status connections
without replying until it has built the status snapshot it serves, which
does not happen until startup has fully finished — slightly after the "Done"
line is logged. 26.1 widened that gap: it saves every dimension after
logging "Done" and before finishing startup, which is the
`Saving chunks for level ...` line only 26.1 prints after `Done`. Since
minecraft-wrap calls back on that line, we ping inside the gap: probing
every 25ms across a boot, 26.1 closes status connections until 52ms past
`Done` and first answers at 77ms, while 1.21.8 already answers 11ms before
it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's startup sequence and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.
rom1504 pushed a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…wers (#3976)

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. A server closes status connections
without replying until it has built the status snapshot it serves, which
does not happen until startup has fully finished — slightly after the "Done"
line is logged. 26.1 widened that gap: it saves every dimension after
logging "Done" and before finishing startup, which is the
`Saving chunks for level ...` line only 26.1 prints after `Done`. Since
minecraft-wrap calls back on that line, we ping inside the gap: probing
every 25ms across a boot, 26.1 closes status connections until 52ms past
`Done` and first answers at 77ms, while 1.21.8 already answers 11ms before
it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's startup sequence and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.
@u9g
u9g deleted the fix-ping-closed-connection branch August 27, 2026 03:50
@u9g

u9g commented Aug 27, 2026

Copy link
Copy Markdown
Member Author

/makerelease

@rom1504bot rom1504bot mentioned this pull request Aug 27, 2026
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…g until the server answers (#4010)

* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* fix(test): retry the external test's status ping until the server answers

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. A server closes status connections
without replying until it has built the status snapshot it serves, which
does not happen until startup has fully finished — slightly after the "Done"
line is logged. 26.1 widened that gap: it saves every dimension after
logging "Done" and before finishing startup, which is the
`Saving chunks for level ...` line only 26.1 prints after `Done`. Since
minecraft-wrap calls back on that line, we ping inside the gap: probing
every 25ms across a boot, 26.1 closes status connections until 52ms past
`Done` and first answers at 77ms, while 1.21.8 already answers 11ms before
it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's startup sequence and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.

* ci: drop the unreleased-fixes install step
minecraft-wrap 1.10.0 and minecraft-protocol 1.68.0 ship these fixes, so the
plain npm install already picks them up via ^1.3.0 and ^1.67.0.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…vent (#4014)

* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* test(consume): eat one bread instead of four and wait on the health event

The eat loop asserted the same usingHeldItem transitions on every
iteration, so refilling to 20 food re-ran an identical code path three
more times at ~1.7s per bread (vanilla's fixed 32-tick eat animation).
Drain also polled on a fixed 500ms sleep and ground hunger all the way
to zero when consuming only requires food below 20.

Now the drain waits on the health event and stops at the first drop,
and a single consume covers the same asserts: 8.9s -> 2.4s on 1.8.8,
~9s -> 3.3s on 26.1.

* ci: drop the unreleased-fixes install step
minecraft-wrap 1.10.0 and minecraft-protocol 1.68.0 ship these fixes, so the
plain npm install already picks them up via ^1.3.0 and ^1.67.0.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* Set test server view-distance to 8 to speed up chunk streaming

The default view-distance of 10 makes the server generate and stream a
21x21 chunk square on every dimension change. The nether test's portal
travel only force-generates the 17x17 square its exit portal scan reads
(±128 blocks), so anything past view-distance 8 is pure overhead: on
1.8.8 the nether test drops from 6.7s to 4.4s with no other test
affected. 8 is also the floor for the suite: blockfinder.js findBlocks
uses maxDistance 128 (exactly 8 chunks), so lower values break the
example tests.

* ci: drop the unreleased-fixes install step
minecraft-wrap 1.10.0 and minecraft-protocol 1.68.0 ship these fixes, so the
plain npm install already picks them up via ^1.3.0 and ^1.67.0.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…0ms (#4013)

* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* Poll for the sign text echo in the nether test instead of sleeping 500ms

Also flattens the test into a linear async flow with no raw bot._client
listener: by the time placeBlock's block update echo has arrived the
server has already opened the sign editor, so the text can be sent
right away — and assertion failures now reject the test naturally
instead of throwing inside a callback.

* Emit blockEntityData and signOpen events and use them in the nether test (#3993)

* Emit blockEntityData and signOpen instead of keeping packets internal

Block entity updates (modern block_entity_data and legacy update_sign)
were stored in the world silently, and the open_sign_entity packet was
not surfaced at all, so consumers had to poll bot.blockAt or reach into
bot._client. Emit a blockEntityData event with the updated block and a
signOpen event with the placed sign instead.

* nether test: use the signOpen and blockEntityData events

Replaces the sign text polling loop with a wait for blockEntityData,
and takes the placed sign from the new signOpen event instead of
reading it out of the world by position.

* ci: drop the unreleased-fixes install step
minecraft-wrap 1.10.0 and minecraft-protocol 1.68.0 ship these fixes, so the
plain npm install already picks them up via ^1.3.0 and ^1.67.0.

* Revert the #3993 squash merge
The blockEntityData/signOpen work belongs to its own PR (#4015); this branch
should carry only the sign-poll change.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…wers (#3976)

The 26.1 external tests fail intermittently in CI with a bare
`Timeout of 120000ms exceeded` in the `before all` hook, after printing
`pinging 26.1 port : N` and never printing `pong`. It is the only version
this happens on: over the last 40 failed CI runs the signature appears 4
times, all on 26.1, never on the other 27 versions.

The server is not answering status yet. A server closes status connections
without replying until it has built the status snapshot it serves, which
does not happen until startup has fully finished — slightly after the "Done"
line is logged. 26.1 widened that gap: it saves every dimension after
logging "Done" and before finishing startup, which is the
`Saving chunks for level ...` line only 26.1 prints after `Done`. Since
minecraft-wrap calls back on that line, we ping inside the gap: probing
every 25ms across a boot, 26.1 closes status connections until 52ms past
`Done` and first answers at 77ms, while 1.21.8 already answers 11ms before
it.

Retry the ping instead of trusting the first one, with a closeTimeout short
enough that a failed attempt is worth retrying. A fixed sleep would be tuned
to today's startup sequence and would break the next time a version adds
work after `Done`.

mc.ping reports this as ETIMEDOUT after its whole closeTimeout, because it
never notices the connection closing; PrismarineJS/node-minecraft-protocol#1514
fixes that upstream and makes each failed attempt immediate.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 27, 2026
…ge (#4007)

* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* test: confirm superflat fills via chat echo instead of sleeping 100ms

The six /fill repairs in resetBlocksToSuperflat are fire-and-forget,
synchronized only by a fixed 100ms sleep — an assumption a slow CI tick
can exceed. A marker chat message sent on the same ordered connection is
echoed back only after the fills have executed and their block updates
have already arrived, so waiting for the echo is both faster and an
actual guarantee.

Measured on 1.8.8: the fills step of the pre-test reset went from
~102ms avg to ~47ms avg (max 66ms).

* chat: on 1.19.0, wait for queued commands before sending a chat message

1.19.0 runs its chat ordering check for commands on the main thread
after queueing the packet, but for chat messages directly on the netty
thread. A message sent while a command is still queued records its
timestamp first, and the server then kicks the bot for that command
with multiplayer.disconnect.out_of_order_chat.

tab_complete is queued to the main thread the same way and always
answered, so a message that follows an unacknowledged command first
waits for a tab_complete reply. Every send on this version goes through
one chain so ordering is preserved.

* chat: gate the 1.19.0 command wait on supportFeature

* Bump minecraft-data to ^3.114.0 for chatCommandsQueuedToMainThread

* Drop the unreleased-dependency install step from CI
rom1504 pushed a commit to PrismarineJS/mineflayer that referenced this pull request Aug 28, 2026
…#3996)

* test: confirm superflat fills via chat echo instead of sleeping 100ms

The six /fill repairs in resetBlocksToSuperflat are fire-and-forget,
synchronized only by a fixed 100ms sleep — an assumption a slow CI tick
can exceed. A marker chat message sent on the same ordered connection is
echoed back only after the fills have executed and their block updates
have already arrived, so waiting for the echo is both faster and an
actual guarantee.

Measured on 1.8.8: the fills step of the pre-test reset went from
~102ms avg to ~47ms avg (max 66ms).

* chat: on 1.19.0, wait for queued commands before sending a chat message (#4007)

* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* test: confirm superflat fills via chat echo instead of sleeping 100ms

The six /fill repairs in resetBlocksToSuperflat are fire-and-forget,
synchronized only by a fixed 100ms sleep — an assumption a slow CI tick
can exceed. A marker chat message sent on the same ordered connection is
echoed back only after the fills have executed and their block updates
have already arrived, so waiting for the echo is both faster and an
actual guarantee.

Measured on 1.8.8: the fills step of the pre-test reset went from
~102ms avg to ~47ms avg (max 66ms).

* chat: on 1.19.0, wait for queued commands before sending a chat message

1.19.0 runs its chat ordering check for commands on the main thread
after queueing the packet, but for chat messages directly on the netty
thread. A message sent while a command is still queued records its
timestamp first, and the server then kicks the bot for that command
with multiplayer.disconnect.out_of_order_chat.

tab_complete is queued to the main thread the same way and always
answered, so a message that follows an unacknowledged command first
waits for a tab_complete reply. Every send on this version goes through
one chain so ordering is preserved.

* chat: gate the 1.19.0 command wait on supportFeature

* Bump minecraft-data to ^3.114.0 for chatCommandsQueuedToMainThread

* Drop the unreleased-dependency install step from CI
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 29, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 29, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 29, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
rom1504 pushed a commit to PrismarineJS/mineflayer that referenced this pull request Aug 30, 2026
* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* test(bed): wait for chunks instead of sleeping a fixed 3s

The bed test opened with `await bot.test.wait(3000)` and the comment "Wait a
few seconds for chunks". mineflayer already exposes the event-based version of
exactly that wait, so use it.

This is both faster and more correct: the fixed sleep over-waits on a fast
machine (the chunks are typically already loaded) and can silently under-wait
on a slow one, where the following blockAt assertions would fail for a reason
unrelated to beds.

Measured against a real 26.1 server, test only:

  before  10922ms
  after    7239ms, 6912ms

* test(bed): reduce comment to the constraint

* ci: drop the unreleased-fixes install step
minecraft-wrap 1.10.0 and minecraft-protocol 1.68.0 ship these fixes, so the
plain npm install already picks them up via ^1.3.0 and ^1.67.0.
rom1504 pushed a commit to PrismarineJS/mineflayer that referenced this pull request Aug 30, 2026
…3999)

* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* test(exampleBee): await the op confirmation instead of sleeping 2s

The 2s sleep after /op bee waited for the op (and with it the spawn
protection bypass) to apply, but the server confirms exactly that with
a chat message. Awaiting the confirmation takes ~50ms on 1.8.8 and
~11ms on 26.1, and is a guarantee where the sleep was a guess:
exampleBee drops from 5.8s to 3.9s, leaving mostly the flight itself.

* Drop the unreleased-dependency install step from CI
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Aug 31, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Sep 4, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Sep 4, 2026
The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.
rom1504 pushed a commit to PrismarineJS/mineflayer that referenced this pull request Sep 5, 2026
…domly; fix rejected quick-move clicks on 1.12–1.16 (#3979)

* ci: install minecraft-wrap from the concurrent-download fix commit

minecraft-wrap 1.9.0 writes the version manifest to a path shared by the
four per-version test processes and reads it back, so they tear each
other's writes and fail in the before-all hook with JSON parse errors.
Install PrismarineJS/node-minecraft-wrap#111 until it is released.

* ci: also install minecraft-protocol from the ping close fix commit

The external test's status-ping retry only helps if a closed status
connection rejects promptly; released minecraft-protocol waits out the
whole closeTimeout instead. Install
PrismarineJS/node-minecraft-protocol#1514 until it is released.

* test(useChests): lower the mouse-click fuzz from 250 to 50

Each bot.clickWindow waits for the server's transaction confirmation, which
arrives on the next tick, so the click loop cannot run faster than 50ms per
click on versions that acknowledge every click.

A packet trace on 1.8.8 shows 366 window_click packets answered 1:1 by
transaction packets, spanning 32149ms of the test's 37898ms — 85% of it. The
version tiers (38.9s up to 1.13.2, 17.6s through 1.16.5, 12.4s from 1.17) track
changes to that acknowledgement protocol, not anything in the test.

Measured on 1.8.8, test duration only:

  before  38323ms
  after   29217ms

* test(useChests): vary the click mode in the fuzz loop

The loop only ever sent mouseButton 0, mode 0, so it reached pick up, place,
merge and swap and nothing else, however many iterations it ran.

Alternate left click, right click and shift-click quick move. Modes 5 and 6 are
assert-unimplemented in prismarine-windows and mode 3 is creative-only, so this
is the reachable set from survival.

Measured, test duration only:

  1.8.8   29217ms -> 28904ms
  26.1               14015ms

* test(useChests): assert each click transition explicitly

A left or right click resolves differently depending on whether the cursor is
holding something and what is in the target slot, so a random click only
reaches whichever case the current state happens to allow. Repeating it does
not change that.

Drive each case from its own precondition and assert the resulting slot and
cursor contents: take a stack, place one, merge into a matching stack, swap
with a different type, drop a whole stack, take half, and shift click out.

The previous loop asserted nothing at all — it only failed if clickWindow threw,
and from 1.17 there is no accept/reject to throw on, only waitForWindowUpdate.

Measured, test duration only: 1.8.8 29721ms, 26.1 15644ms.

* test(useChests): drop the clicks that assert nothing

Of the 366 window clicks the test made, ~350 asserted nothing: the random
layout filled ~51 slots purely so the random click loop had something to click,
and that loop had no assertions of its own — from 1.17 there is not even an
accept/reject for it to throw on.

The transition cases need one stack of more than 2, a second stack of a
different type, and a free slot, so place three items deterministically and
drop the fuzz loop. Right click and shift click stay covered, with assertions,
in testClickTransitions.

Measured, test duration only:

  1.8.8   38323ms -> 14746ms   (366 clicks -> 33)
  26.1               14376ms

* test(useChests): fill the chest server side, not by clicking

Filling it with /give plus moveSlotItem leaves the result at the mercy of the
client's predicted state. From 1.17 a click is not confirmed per transaction,
so a lost move is invisible until an assertion reads the slot, and on 26.1 two
of the three items silently never arrived: the chest held only the last one.

The previous ~51-item random layout hid this — with that many items, losing a
few still left plenty satisfying the preconditions. Three items removed the
margin.

Write the slots with /item replace block (1.17+), /replaceitem block (1.13+) or
slot.container (older) instead, and assert the contents once the window opens,
so a wrong command fails loudly rather than silently under-filling.

Measured on 1.8.8: 13777ms.

* test(useChests): find the filled items instead of assuming which half of the double chest lists first

* fix: send an empty item for quick-move clicks on 1.12-1.16 so the server accepts them

For actionIdUsed versions the server accepts a click only if the packet's
item matches the result of its own slotClick. For a mode 1 quick move that
is the moved stack up to 1.11 and empty from 1.12, and mineflayer always sent
the clicked stack, so every shift click was rejected on 1.12-1.16. The old
useChests fuzz never sent mode 1, so nothing noticed.

* test(useChests): turn instantly

openContainer and placeBlock face the block before acting, and a non-forced
look turns at 3 rad/s, so the 180 degree swings between the chests cost
about 1s per open. Nothing here asserts on looking.

Measured, test duration only, 1.8.8: 13445ms -> 3475ms.

* Drop the unreleased-dependency install step from CI

* Use supportFeature instead of version comparisons

quickMoveClickSendsEmptyItem, hasItemCommand and replaceItemSlotIsPrefixed
are added in PrismarineJS/minecraft-data#1262.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants