Answer server-to-client pings in the client - #541
Merged
koic merged 1 commit intoSep 4, 2026
Conversation
## Motivation and Context
The ping utility says the receiver "MUST respond promptly with an empty response",
and a server may treat a missing answer as a stale connection and drop it. Both reference SDKs honor
that on the client side (the TypeScript protocol layer installs an automatic pong,
and the Python session answers `PingRequest` with an empty result), but this client answered
a server ping with Method not found over Streamable HTTP and ignored it entirely over stdio,
leaving long-lived legacy sessions vulnerable to a liveness-checking server.
Over HTTP, an unhandled `ping` is now answered with the empty result in `dispatch_server_request`.
The default deliberately does not live in `@server_request_handlers`: that registry's `any?` opens
the GET listening stream on connect, and a built-in pong must not change listener behavior -
a server can only ping on a stream that is already open. A handler registered via
`on_server_request("ping")` still wins.
Over stdio, the read loop now answers a ping frame inline and keeps waiting for its response.
Frames are told apart by shape rather than id: one carrying `method` is a request or notification,
never the awaited response, so a server-chosen ping id that collides with the awaited one is still answered -
and a request frame can no longer be returned to the caller as if it were the response.
Between requests nothing reads stdout, so a queued ping is answered when the next request starts reading,
which is inherent to the single-threaded stdio client. Other server-to-client requests over stdio stay
unsupported as documented.
The pong is best effort on both transports, in the ways the reference SDKs imply: a pong that cannot be written
over a broken stdin is dropped instead of failing the unrelated request whose response the loop is reading,
and a ping whose id is not a String or a Number - a shape the TypeScript and Python schemas reject outright -
is not answered with a pong: over stdio it is skipped entirely, and over HTTP it falls through to Method not found,
exactly as before this change.
The modern lifecycle removes `ping` altogether (SEP-2575), so this applies to handshake-lifecycle connections only.
## How Has This Been Tested?
New transport tests: over HTTP, a ping delivered on the SSE stream results in a POSTed empty-result response with
the ping's id, and a user-registered `"ping"` handler overrides the default; over stdio, a ping injected while
a response is awaited is answered on stdin and the original response still returns. Both fail without the change
(Method not found / no answer). Further tests cover the best-effort edges: a pong write failure over
a closed stdin leaves the awaited response intact, and a malformed-id ping is skipped over stdio
and answered with Method not found over HTTP. `bundle exec rake` (tests, RuboCop, and conformance baseline) passes.
## Breaking Changes
None. A server ping that previously received Method not found (or nothing) now receives the empty result
the specification requires.
atesgoral
approved these changes
Sep 4, 2026
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.
Motivation and Context
The ping utility says the receiver "MUST respond promptly with an empty response", and a server may treat a missing answer as a stale connection and drop it. Both reference SDKs honor that on the client side (the TypeScript protocol layer installs an automatic pong, and the Python session answers
PingRequestwith an empty result), but this client answered a server ping with Method not found over Streamable HTTP and ignored it entirely over stdio, leaving long-lived legacy sessions vulnerable to a liveness-checking server.Over HTTP, an unhandled
pingis now answered with the empty result indispatch_server_request. The default deliberately does not live in@server_request_handlers: that registry'sany?opens the GET listening stream on connect, and a built-in pong must not change listener behavior - a server can only ping on a stream that is already open. A handler registered viaon_server_request("ping")still wins.Over stdio, the read loop now answers a ping frame inline and keeps waiting for its response. Frames are told apart by shape rather than id: one carrying
methodis a request or notification, never the awaited response, so a server-chosen ping id that collides with the awaited one is still answered - and a request frame can no longer be returned to the caller as if it were the response. Between requests nothing reads stdout, so a queued ping is answered when the next request starts reading, which is inherent to the single-threaded stdio client. Other server-to-client requests over stdio stay unsupported as documented.The pong is best effort on both transports, in the ways the reference SDKs imply: a pong that cannot be written over a broken stdin is dropped instead of failing the unrelated request whose response the loop is reading, and a ping whose id is not a String or a Number - a shape the TypeScript and Python schemas reject outright - is not answered with a pong: over stdio it is skipped entirely, and over HTTP it falls through to Method not found, exactly as before this change.
The modern lifecycle removes
pingaltogether (SEP-2575), so this applies to handshake-lifecycle connections only.How Has This Been Tested?
New transport tests: over HTTP, a ping delivered on the SSE stream results in a POSTed empty-result response with the ping's id, and a user-registered
"ping"handler overrides the default; over stdio, a ping injected while a response is awaited is answered on stdin and the original response still returns. Both fail without the change (Method not found / no answer). Further tests cover the best-effort edges: a pong write failure over a closed stdin leaves the awaited response intact, and a malformed-id ping is skipped over stdio and answered with Method not found over HTTP.bundle exec rake(tests, RuboCop, and conformance baseline) passes.Breaking Changes
None. A server ping that previously received Method not found (or nothing) now receives the empty result the specification requires.
Types of changes
Checklist