server: make a missing length delimiter recoverable, not fatal (#29) - #48
Conversation
…ev#29) The client already treated delimiter_missing_message (DelimiterMissingBufferResend) as a resend trigger, but ServerSettings had no such field and the server raised BluetoothServerError('Invalid length prefix') on a frame with no ':' — ending the session and leaving the client branch unreachable. Decision: a missing/invalid length prefix is recoverable (a transient malformed frame should not tear down the session), matching how empty and corrupt buffers are already handled. Add delimiter_missing_message to ServerSettings and have the server request a resend and continue instead of raising. Client and server now agree on the full protocol message set. Adds a round-trip test.
sraodev
left a comment
There was a problem hiding this comment.
Reviewed the diff and ran the real BluetoothServer/BluetoothClient against each other over a fragmenting in-memory stream to check the liveness question. Detailed findings are inline.
The decision is the right one, and the change fixes a silent data-corruption bug that the issue didn't even ask about: on master, b"-1:data" parsed to data_size = -1, passed the len(remainder) < data_size check, and the server acked while handing a truncated b"dat" to the deserializer. isdigit() closes that. Verified end to end.
One blocking issue (inline on server.py): the continue removes the only condition that terminated a desynchronized stream, so an ordinary MTU-fragmented frame now hangs instead of erroring.
Two findings that can't be anchored to the diff:
sdk/python/tests/never runs in CI..github/workflows/ci.ymlrunsscripts/smoke.pyandexamples/chatonly; no job invokes pytest against the SDK tests. There's also noconftest.pyor requirements file, so collection fails without PyBluez (bluetooth_service/client_socket.py:9importsbluetoothat module scope). The new test ships with no regression protection. Corroborating symptom:tests/test_client.py:73assertsb"14:payload-bytes"against a 13-byte payload and fails onmastertoo — pre-existing, and only invisible because nothing runs these tests.- The client branch this PR makes reachable is still untested.
tests/test_client.py:79covers onlyEmptyBufferResend. Issue #29's fourth acceptance criterion is that both peers agree on the message set; the server half is done, the client half is unverified.
Also: issue #29's first acceptance criterion asks for the decision to be recorded on the issue. It currently has no comments — the rationale lives only in this PR description.
| # rather than tearing the connection down. The client already | ||
| # treats delimiter_missing_message as a resend trigger. | ||
| logger.warning("Missing/invalid length prefix; requesting resend") | ||
| self._socket_manager.send(self.settings.delimiter_missing_message) |
There was a problem hiding this comment.
The server doesn't drain the in-flight frame before looping, so it emits one control message per leftover chunk instead of one per client frame. The control channel is itself unframed, so the client's single recv coalesces them and the exact-match set test at client.py:61 fails.
Observed directly in the fragmentation run above:
BluetoothServerError("Unexpected acknowledgement:
b'CorruptedBufferResendDelimiterMissingBufferResend...")
The fault has moved from the server (clear message, at the point of failure) to the client (unrecognizable concatenation), while the server carries on believing the session is healthy. Distinct from the cap — worth resyncing after a resend request and emitting at most one control message per client frame.
There was a problem hiding this comment.
Leaving this open as a follow-up rather than a blocker — the resend budget bounds it now, so it fails loudly instead of hanging. Still real: the server does not drain the in-flight frame, so one oversized client frame produces several control messages, and the unframed control channel lets the client coalesce them into Unexpected acknowledgement: b"CorruptedBufferResendDelimiterMissing...". The proper fix is accumulating across recv until data_size bytes are present, which is a framing change, not a patch to this PR.
| # Acknowledgement / retry protocol messages | ||
| resend_empty_message: str = "EmptyBufferResend" | ||
| resend_corrupt_message: str = "CorruptedBufferResend" | ||
| delimiter_missing_message: str = "DelimiterMissingBufferResend" |
There was a problem hiding this comment.
Correct fix for the issue as filed. Worth noting the underlying hazard this PR is evidence of: the four protocol strings are now duplicated between ServerSettings here and ClientSettings at client_config.py:23-26, with nothing asserting the two agree.
That's not hypothetical — it's the cause of issue #29 itself. client_config.py:25 and the client.py:64 branch have existed since d2319c6 while no server ever sent the message, so the two halves of one wire protocol drifted for an entire release and nothing caught it. If either literal drifts again, every resend request silently becomes BluetoothServerError(f"Unexpected acknowledgement: ...") at client.py:72.
These are protocol constants, not per-side configuration. A shared definition both dataclasses reference would make the class of bug impossible to ship. Fine as a follow-up, but the duplication is now 3 strings deep.
There was a problem hiding this comment.
Follow-up, not a blocker for this PR. Tracking the shared-constant refactor separately.
|
Addressed the review in bd86777.
Verification: all 14 tests under |
|
Thanks for picking this one up, @matthew6s — and for writing up the reasoning rather than just the patch. Making the missing delimiter recoverable was the right call, and pointing at Worth calling out something the change does that the issue never asked for: switching from The one thing that needs to change before this merges is in my inline comment on the
I have a patch that fixes it and I'll get it onto this branch shortly — it adds It also parametrizes the test over Nothing here needs action from you unless you'd rather make these changes yourself, in which case say so and I'll leave it to you. The remaining inline comments (stream resync / control-message framing, and the duplicated protocol constants) are follow-up issues, not blockers for this PR. |
|
Looks like our updates crossed by about a minute — I pushed bd86777 just before this comment landed. It implements the same bounded-retry shape, parametrized malformed-prefix coverage (including The only intentional difference from the patch you described is the default retry budget: I used 3 rather than 5. Happy to align it to 5 (and your preferred error wording) if you want that convention here; otherwise the branch should already cover the blocker, so you shouldn't need to duplicate the patch. |
Fixes #29.
Decision (per the issue): a missing/invalid length delimiter is recoverable. A transient malformed frame shouldn't tear down the session — and the client already treats
delimiter_missing_message(DelimiterMissingBufferResend) as a resend trigger, so making the server send it is the smaller, more consistent change (it mirrors the existing empty/corrupt resend paths).Changes:
ServerSettingsgainsdelimiter_missing_message(matching the client's value), so client and server now agree on the full protocol message set._receive_buffer_with_ackno longer raisesBluetoothServerError('Invalid length prefix')on a frame with no:(or a non-numeric prefix); it sends the resend request and continues.test_server_requests_resend_on_missing_delimiterdriving the round trip (malformed frame → resend sent → valid frame → ack + payload).All server tests pass locally.