fix(ping): reject when the connection closes before a status response - #1514
Open
u9g wants to merge 1 commit into
Open
fix(ping): reject when the connection closes before a status response#1514u9g wants to merge 1 commit into
u9g wants to merge 1 commit into
Conversation
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
force-pushed
the
fix-ping-closed-connection
branch
from
August 22, 2026 19:53
c261f43 to
79bae6f
Compare
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
force-pushed
the
fix-ping-closed-connection
branch
from
August 22, 2026 19:58
79bae6f to
66bcc35
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ping()settles on exactly three things:server_info,ping, or a clienterror. A server that accepts the connection and then closes it without answering is invisible to all three — the promise sits untilcloseTimeout(120 s by default) and then rejects withETIMEDOUT, 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
Doneline 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
endwhen no status has arrived yet:If the status did arrive and the server closes before the pong, the existing
noPongTimeoutpath 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.jspoints a ping at a socket server that accepts the handshake and hangs up. Without the fix it fails after the fullcloseTimeoutwithETIMEDOUT; with it, it fails in ~400 ms with a message that says what happened.