You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
GATT server write path cannot propagate application errors to the client (handleGattEvent always returns 0) #441
When an application rejects a write inside NimBLECharacteristicCallbacks::onWrite (e.g. device busy, validation failure), the BLE client still receives a successful Write Response. There is no way to signal an ATT Error Response for a rejected write.
Root cause
The NimBLE C stack supports ATT Error Responses from the ble_gatt_access_fn callback (its return value is int), and a characteristic with the WRITE property uses Write Requests (opcode 0x12), which per the Bluetooth spec support error responses. The capability is lost in the C++ wrapper, in three places:
NimBLECharacteristicCallbacks::onWrite is void.
NimBLECharacteristic::writeEvent is void.
NimBLEServer::handleGattEvent (src/NimBLEServer.cpp, the OP_WRITE_DSC/OP_WRITE_CHR block) ends with a hardcoded return 0; right after pAtt->writeEvent(buf, len, peerInfo);.
So business-layer results (which are computed synchronously inside the ATT transaction context by applications) are silently discarded, and every write is ACKed as success.
Constraint
writeEvent cannot simply be changed to return int: it is a pure virtual in NimBLELocalValueAttribute (virtual void writeEvent(...) = 0) and overridden by both NimBLECharacteristic and NimBLEDescriptor. Changing its signature is a breaking API change.
Proposed direction
Keep all signatures unchanged and add a per-write error slot on NimBLECharacteristic:
void setWriteError(uint8_t attError) — called by the application inside onWrite(); 0 accepts, vendor range 0x80–0x9F for application-defined errors.
int getWriteError() const — read by handleGattEvent's write branch, which returns it instead of 0.
writeEvent() resets the slot to 0 before invoking onWrite, and the OP_WRITE_DSC path keeps returning 0 (descriptors carry no slot). Fully backwards compatible: applications that never call setWriteError see identical behavior.
PR
Implemented in #442. Feedback on the design (especially whether you'd prefer a different mechanism, e.g. a new virtual with a default implementation) is welcome.
@h2zero - the fix is implemented in #442 (master) and backported to #444 (release/2.5). We're using this in production via our fork. Would appreciate a review/merge when you have cycles.
Activity
jie-meng commentedon Sep 3, 2026
@h2zero - the fix is implemented in #442 (master) and backported to #444 (release/2.5). We're using this in production via our fork. Would appreciate a review/merge when you have cycles.