ae.utils.jsonrpc: Ignore unknown members in protocol objects - #60
Merged
CyberShadow merged 1 commit intoJul 24, 2026
Merged
Conversation
JSON-RPC peers add members to the objects they send as their protocols evolve. The JSON-RPC 2.0 specification enumerates the members of the request, response and error objects, but does not require an implementation to reject additional ones, so a codec that rejects them cannot interoperate across peer versions. Rejecting them is also disproportionately severe here: deserialization throws, and JsonRpcCodec.processRequests translates that into assert(false), which is a halt instruction in a release build. One unrecognized member from a peer therefore terminates the process, and without a message. A real instance: openai-codex 0.145.0 emits an app-server notification carrying a top-level "emittedAtMs" member, which halts any ae-based client that reads it. Annotate the three peer-deserialized structs with @JSONPartial so that unknown members are drained instead. The accompanying test in ae.net.jsonrpc.codec feeds such a request through the codec's read handler, single and batched, and asserts that both dispatch without disconnecting; without the annotations it fails at the assert(false).
Owner
|
Thanks! |
Owner
|
CyDo bumped: CyberShadow/CyDo@013f08b |
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.
JSON-RPC peers gain members in the objects they send as their protocols evolve. The JSON-RPC 2.0 specification enumerates the members of the request, response and error objects, but does not require an implementation to reject additional ones, so a codec that rejects them cannot interoperate across peer versions.
Here the rejection is also disproportionately severe.
deserializeTo!JsonRpcRequestthrowsUnknown field <name>, andJsonRpcCodec.processRequeststranslates that into an assert:In a release build
assert(false)is a halt instruction, so one unrecognized member from a peer terminates the whole process, and without a message. For contrast,processMessagetwo functions above handles the same class of failure withdoDisconnect("Malformed message: " ~ e.msg).Trigger seen in the wild
openai-codex0.145.0 emits anapp-servernotification of this shape (host-specific values elided):{"method":"remoteControl/status/changed", "params":{"status":"disabled","serverName":"...", "installationId":"...","environmentId":null}, "emittedAtMs":1784741558897}The top-level
emittedAtMsmember is not part of the JSON-RPC request object. An ae-based client reading that notification dies withSIGILL(si_code: ILL_ILLOPN) the moment it arrives, and since the halt carries no message, nothing is logged to explain it.The same notification also omits
"jsonrpc":"2.0", which the specification does require. That part is already harmless: the field simply defaults toJSONRPC_VERSION, and is not validated.The change
@JSONPartialonJsonRpcRequest,JsonRpcResponseandJsonRpcError, the three structs deserialized from peer input, so unknown members are drained rather than throwing.Test
A
debug(ae_unittest) unittestinae.net.jsonrpc.codecdrives the codec through its connection's read handler with a request carrying an unknown member, both single and batched, and asserts that both dispatch without the connection being disconnected.Removing the three annotations makes that test fail at the
assert(false)inprocessRequests, so it covers exactly the path described above rather than just the deserializer.dub test --debug=ae_unittest: 146 modules pass.Scope
This only stops the codec from rejecting such messages. It deliberately leaves the
assert(false)alone, since that is a separate concern: a genuinely undeserializable request still halts the process instead of dropping the one connection. Happy to follow up on that separately if you want it changed.