Add onReadStatus/onWriteStatus callbacks so an application can reject a read/write with an ATT error#429
Add onReadStatus/onWriteStatus callbacks so an application can reject a read/write with an ATT error#429Nicba1010 wants to merge 2 commits into
Conversation
The GATT access handler discarded the outcome of the characteristic write callback and always returned 0 (success) to the peer, so an application had no way to reject a write with an ATT error even under write-with-response. Add opt-in status-returning callbacks onReadStatus/onWriteStatus to NimBLECharacteristicCallbacks that default to invoking the existing void onRead/onWrite and returning 0, preserving current behavior for all existing overrides. Thread an int return up through the internal readEvent/writeEvent chain (NimBLELocalValueAttribute, NimBLECharacteristic, NimBLEDescriptor) so handleGattEvent forwards a non-zero BLE_ATT_ERR_* code to the host, which turns it into an ATT error response. A rejection reaches the peer only for write-with-response; a write command is unacknowledged so the code is dropped by the stack, by spec.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughArrr, this PR turns GATT read/write handlers into status-returning paths across ChangesStatus-returning read/write events
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant NimBLEServer
participant NimBLECharacteristic
participant NimBLECharacteristicCallbacks
NimBLEServer->>NimBLECharacteristic: readEvent(connInfo)
NimBLECharacteristic->>NimBLECharacteristicCallbacks: onReadStatus(this, connInfo)
NimBLECharacteristicCallbacks-->>NimBLECharacteristic: status code
NimBLECharacteristic-->>NimBLEServer: status code (reject if non-zero)
NimBLEServer->>NimBLECharacteristic: writeEvent(val, len, connInfo)
NimBLECharacteristic->>NimBLECharacteristic: setValue(val, len)
NimBLECharacteristic->>NimBLECharacteristicCallbacks: onWriteStatus(this, connInfo)
NimBLECharacteristicCallbacks-->>NimBLECharacteristic: status code
NimBLECharacteristic-->>NimBLEServer: status code returned directly
Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/NimBLECharacteristic.h (1)
331-349: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winArr, append new virtual callbacks after the existing ones.
These new virtuals are inserted before
onStatus/onSubscribe, shifting existing vtable slots for consumers using precompiled objects. If binary compatibility matters, place the new virtuals after the existing callback declarations or document the ABI break.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/NimBLECharacteristic.h` around lines 331 - 349, The new virtual callbacks in NimBLECharacteristic are being inserted before existing callbacks and will shift vtable layout for callers using precompiled objects. Move onReadStatus and onWriteStatus in NimBLECharacteristic so they are declared after the existing callback methods such as onStatus and onSubscribe, or otherwise clearly document the ABI break in the class interface.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/NimBLECharacteristic.cpp`:
- Around line 435-437: The write path in NimBLECharacteristic::writeEvent
currently commits the new payload via setValue() before
m_pCallbacks->onWriteStatus() decides whether the write is accepted, so rejected
writes still update local state. Change writeEvent to preserve the previous
characteristic value, call onWriteStatus() first or otherwise detect a
non-success BLE_ATT_ERR_* result, and restore the prior value if the write is
rejected; use the NimBLECharacteristic::writeEvent and setValue() flow as the
place to implement the rollback.
---
Nitpick comments:
In `@src/NimBLECharacteristic.h`:
- Around line 331-349: The new virtual callbacks in NimBLECharacteristic are
being inserted before existing callbacks and will shift vtable layout for
callers using precompiled objects. Move onReadStatus and onWriteStatus in
NimBLECharacteristic so they are declared after the existing callback methods
such as onStatus and onSubscribe, or otherwise clearly document the ABI break in
the class interface.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 03dd77e4-3543-4489-8546-dee43af446dc
📒 Files selected for processing (6)
src/NimBLECharacteristic.cppsrc/NimBLECharacteristic.hsrc/NimBLEDescriptor.cppsrc/NimBLEDescriptor.hsrc/NimBLELocalValueAttribute.hsrc/NimBLEServer.cpp
writeEvent() committed setValue() before invoking the callback, so a callback returning a BLE_ATT_ERR_* left the rejected payload as the stored value: the peer saw an error while local state had accepted the write (observable on any readable characteristic or via getValue()). Snapshot the value before the tentative commit and restore it when onWriteStatus() rejects. The commit-before-callback ordering is kept so getValue() still reflects the new value inside the callback (the default onWriteStatus calls the legacy onWrite). NimBLEAttValue copy/assign are deep copies, so the snapshot/restore is self-contained.
Add onReadStatus/onWriteStatus callbacks so an application can reject a read/write with an ATT error
Addresses #428.
Short version of the problem (full detail in the issue):
onWrite()returnsvoidand
handleGattEventhardcodes a success return on the write path, so there's no wayfor an application to reject a write and have the peer receive an ATT error — even under
write-with-response, where the protocol allows it. The C host honors a non-zero return
from the access callback; the wrapper just doesn't expose that to application code.
What this does
Adds two opt-in, status-returning callbacks to
NimBLECharacteristicCallbacks:Each returns
0to accept or aBLE_ATT_ERR_*code to reject. Their defaultimplementations call the existing
onRead/onWriteand return0, so behavior isunchanged for anyone who doesn't override them. Internally the
readEvent/writeEventchain (
NimBLELocalValueAttribute→NimBLECharacteristic/NimBLEDescriptor) nowreturns
int, andhandleGattEventforwards that code to the host instead of alwaysreturning
0. On a rejected read it returns before appending the value.Six files, +51/-15. No public signature changes to existing methods.
Backward compatibility
Nothing existing changes shape.
onRead/onWritekeep theirvoidsignatures, andevery current override keeps working untouched — the new
onReadStatus/onWriteStatusdefault to invoking them and accepting. It's purely additive.
I went with the additive form because it drops into a minor release without touching
anyone. That said — since master is already
3.0.0-dev, if you'd rather take the cleanerbreaking version for the major (just change
onRead/onWritethemselves to returnint,0= accept, and drop the separate status methods), I'm happy to reshape this into that.It mirrors the C stack one-to-one; the only reason I didn't lead with it is the override
break. Your call on which fits 3.0 better.
One caveat
A rejection only reaches the peer under write-with-response. A write-without-response
(ATT Write Command) is unacknowledged, so the stack drops the code and sends nothing —
that's per spec, not something this changes. Worth a mention in the docs so nobody expects
a rejection on a
WRITE_NO_RSPcharacteristic.Descriptors are compile-updated to the new
intreturn but always accept; the sameonWriteStatus-style hook could be added toNimBLEDescriptorCallbackslater if there'sdemand.
Testing
Builds clean for
esp32s3. I also validated it end-to-end on an ESP32-S3 with a macOScentral: a characteristic returning
BLE_ATT_ERR_INSUFFICIENT_RESfromonWriteStatussurfaced on the central as
CBATTErrorDomain Code=17 "Resources are insufficient."underwrite-with-response, and produced no error under write-without-response — matching the
caveat above. Existing writes to unmodified characteristics were unaffected.
There's an open question from the issue worth settling here too:
writeEventcommitssetValue()before the callback runs, so on a rejected write the local value has alreadybeen updated. This PR keeps that ordering; if you'd prefer the pending
(data, len)bepassed to
onWriteStatusso an app can validate before commit, I can add that.AI usage disclosure
For transparency: I used an AI assistant while working on this — to help trace the call
path through the C host stack, cross-check the behavior against the source, and draft the
code. Flagging that so it's on the table. The changes are small and the cited code paths
point at specific locations, so they're straightforward to review against the tree.
Summary by CodeRabbit