cultmesh-py: a put that could never be stored is refused, not raised
_unservable_put sized a put by building the envelope it would store, and that read document["recordKey"] and document["payload"] directly: a put over RUDP with no recordKey raised KeyError out of the RUDP loop and ended the thread for every peer. A string payload raised TypeError the same way. _unservable_put now refuses a put whose document lacks a string recordKey or a binary payload as malformed_document_put, the code the server already uses for a put with no document map, before any sizing reads it and before its early return, so the TCP path refuses it too instead of raising in apply_raw_put_message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d04d71e898
commit
1a173f6000
2 changed files with 48 additions and 1 deletions
|
|
@ -646,7 +646,19 @@ class CultMeshLocalServer:
|
|||
"""The refusal for a put this server could never serve, or None. The document alone,
|
||||
as a snapshot would serve it under the shortest message id, must fit max_snapshot_bytes;
|
||||
a put arriving over RUDP must also fit, as fragments of rudp_max_fragment_bytes, in one
|
||||
message and the peer's reliable queue."""
|
||||
message and the peer's reliable queue. A put whose document has no string recordKey or
|
||||
binary payload could never be stored or served, and is refused as malformed."""
|
||||
document_record = message.get("document")
|
||||
if not (
|
||||
isinstance(document_record, dict)
|
||||
and isinstance(document_record.get("recordKey"), str)
|
||||
and isinstance(document_record.get("payload"), (bytes, bytearray))
|
||||
):
|
||||
return self._error_response(
|
||||
"Raw put documents must carry a string recordKey and a binary payload.",
|
||||
message_id=str(message.get("messageId") or ""),
|
||||
code="malformed_document_put",
|
||||
)
|
||||
if self.max_snapshot_bytes is None and not over_rudp:
|
||||
return None
|
||||
alone = self.node.database.raw_put_served_alone(message, message_id=_SHORTEST_SERVED_MESSAGE_ID)
|
||||
|
|
|
|||
|
|
@ -4585,6 +4585,41 @@ class CultMeshTests(unittest.TestCase):
|
|||
self.assertEqual(hello_reply["schemaVersion"], "cultnet.hello.v0")
|
||||
self.assertTrue(alive)
|
||||
|
||||
def test_cultmesh_local_server_refuses_a_malformed_put_over_rudp_and_serves_on(self) -> None:
|
||||
# A put whose document has no string recordKey or no binary payload could never be stored
|
||||
# or served: it is refused as malformed before any sizing reads it, and the RUDP thread
|
||||
# serves the next peer.
|
||||
document = self._rudp_bound_note()
|
||||
node = CultMesh.create_node(runtime_id="mesh-rudp-malformed")
|
||||
node.database.register_document(document)
|
||||
server = CultMesh.serve_node(node)
|
||||
try:
|
||||
refusals = []
|
||||
for field_name, value in (
|
||||
("recordKey", None),
|
||||
("recordKey", 5),
|
||||
("payload", None),
|
||||
("payload", "not binary"),
|
||||
):
|
||||
put = self._rudp_note_put(document, f"bad-{field_name}-{value}", "note:bad", 10)
|
||||
if value is None:
|
||||
del put["document"][field_name]
|
||||
else:
|
||||
put["document"][field_name] = value
|
||||
refusal, _ = self._rudp_exchange(server, put)
|
||||
refusals.append((put["messageId"], refusal))
|
||||
after, _ = self._rudp_exchange(server, snapshot_request(message_id="after").to_wire())
|
||||
alive = server._rudp_thread is not None and server._rudp_thread.is_alive()
|
||||
finally:
|
||||
server.stop()
|
||||
for message_id, refusal in refusals:
|
||||
self.assertEqual(refusal["schemaVersion"], "cultnet.error.v0", message_id)
|
||||
self.assertEqual(refusal["messageId"], message_id)
|
||||
self.assertEqual(refusal["code"], "malformed_document_put", message_id)
|
||||
self.assertEqual(after["messageId"], "after")
|
||||
self.assertEqual(after["documents"], [])
|
||||
self.assertTrue(alive)
|
||||
|
||||
def test_cultmesh_local_server_refuses_options_that_could_answer_nothing(self) -> None:
|
||||
# The Rust document server's option rules: every limit is greater than zero, and the limits
|
||||
# leave room for an empty snapshot, the smallest response the server sends. One-byte
|
||||
|
|
|
|||
Loading…
Reference in a new issue