You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mynewt.apache.org by ja...@apache.org on 2026/10/23 22:02:10 UTC
(mynewt-newtmgr) 04/20: nmxact/nmcoap: Fix loss of back-to-back CoAP-TCP messages
This is an automated email from the ASF dual-hosted git repository.
sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newtmgr.git
commit d4a9509200d4709968c00eb1b261ddfc77510293
Author: Szymon Janc <sz...@codecoup.pl>
AuthorDate: Thu Oct 22 00:24:39 2026 +0200
nmxact/nmcoap: Fix loss of back-to-back CoAP-TCP messages
Reassembler.RxFrag stored the unread remainder returned by
coap.PullTcp but then unconditionally cleared it after extracting a
message. If a received chunk contained the end of one message and the
start (or all) of the next one, the second message was silently
dropped, and with it the response a caller was waiting for, which then
timed out.
Keep the remainder after a successful pull. Since RxFrag returns only
one message per call, also add Receiver.RxBuffered and make
Dispatcher.Dispatch keep dispatching until no complete message is left
in the buffer, so a second complete message is delivered right away
instead of waiting for the next fragment. Only CoAP-TCP (BLE) buffers
data; UDP behaviour is unchanged.
Assisted-by: Claude:claude-opus-5-5
---
nmxact/nmcoap/dispatch.go | 16 ++++++++---
nmxact/nmcoap/dispatch_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++
nmxact/nmcoap/frag.go | 5 +++-
nmxact/nmcoap/frag_test.go | 50 ++++++++++++++++++++++++++++++++
nmxact/nmcoap/receiver.go | 15 ++++++++++
5 files changed, 146 insertions(+), 5 deletions(-)
diff --git a/nmxact/nmcoap/dispatch.go b/nmxact/nmcoap/dispatch.go
index 4048cd9..c8a3e71 100644
--- a/nmxact/nmcoap/dispatch.go
+++ b/nmxact/nmcoap/dispatch.go
@@ -104,12 +104,20 @@ func (d *Dispatcher) RemoveListener(mc MsgCriteria) *Listener {
// Returns true if the response was dispatched.
func (d *Dispatcher) Dispatch(data []byte) bool {
- // See if this fragment completes a packet.
- msg := d.rxer.Rx(data)
- if msg == nil {
- return false
+ dispatched := false
+
+ // See if this fragment completes a packet. With TCP, a fragment may
+ // complete more than one packet; dispatch all of them.
+ for msg := d.rxer.Rx(data); msg != nil; msg = d.rxer.RxBuffered() {
+ if d.dispatchMsg(msg) {
+ dispatched = true
+ }
}
+ return dispatched
+}
+
+func (d *Dispatcher) dispatchMsg(msg coap.Message) bool {
d.mtx.Lock()
defer d.mtx.Unlock()
diff --git a/nmxact/nmcoap/dispatch_test.go b/nmxact/nmcoap/dispatch_test.go
new file mode 100644
index 0000000..a8df620
--- /dev/null
+++ b/nmxact/nmcoap/dispatch_test.go
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package nmcoap
+
+import (
+ "testing"
+)
+
+// A single TCP chunk containing two complete messages must dispatch both
+// of them immediately, not just the first one.
+func TestDispatcherTcpBackToBack(t *testing.T) {
+ d := NewDispatcher(true, 0)
+
+ l1, err := d.AddListener(MsgCriteria{Token: []byte{1}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ l2, err := d.AddListener(MsgCriteria{Token: []byte{2}})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer d.RemoveListener(l1.Criteria)
+ defer d.RemoveListener(l2.Criteria)
+
+ b1 := tcpMsg(t, []byte{1}, "", []byte("one"))
+ b2 := tcpMsg(t, []byte{2}, "", []byte("two"))
+ if !d.Dispatch(append(append([]byte{}, b1...), b2...)) {
+ t.Fatalf("Dispatch() = false, want true")
+ }
+
+ for i, l := range []*Listener{l1, l2} {
+ select {
+ case m := <-l.RspChan:
+ if m.Token()[0] != byte(i+1) {
+ t.Fatalf("listener %d got token % x", i+1, m.Token())
+ }
+ default:
+ t.Fatalf("listener %d did not receive its message", i+1)
+ }
+ }
+}
+
+func TestDispatcherNoListener(t *testing.T) {
+ d := NewDispatcher(true, 0)
+ if d.Dispatch(tcpMsg(t, []byte{1}, "", nil)) {
+ t.Fatalf("Dispatch() = true without a listener")
+ }
+}
diff --git a/nmxact/nmcoap/frag.go b/nmxact/nmcoap/frag.go
index a7371d7..673da9d 100644
--- a/nmxact/nmcoap/frag.go
+++ b/nmxact/nmcoap/frag.go
@@ -48,6 +48,9 @@ func (r *Reassembler) RxFrag(frag []byte) *coap.TcpMessage {
return nil
}
- r.cur = nil
+ // Keep any trailing bytes; they are the start of the next message.
+ if len(r.cur) == 0 {
+ r.cur = nil
+ }
return tm
}
diff --git a/nmxact/nmcoap/frag_test.go b/nmxact/nmcoap/frag_test.go
index 7a3a3ff..7aefb98 100644
--- a/nmxact/nmcoap/frag_test.go
+++ b/nmxact/nmcoap/frag_test.go
@@ -159,3 +159,53 @@ func TestReceiverTcp(t *testing.T) {
t.Fatalf("unexpected message %v", got)
}
}
+
+// Two messages received in a single buffer must both be returned, one per
+// call. Previously the second message was silently dropped.
+func TestReassemblerBackToBack(t *testing.T) {
+ b1 := tcpMsg(t, []byte{1}, "", []byte("first"))
+ b2 := tcpMsg(t, []byte{2}, "", []byte("second"))
+ b3 := tcpMsg(t, []byte{3}, "", []byte("third"))
+
+ r := NewReassembler()
+ buf := append(append([]byte{}, b1...), b2...)
+
+ checkTcpMsg(t, r.RxFrag(buf), []byte{1}, []byte("first"))
+ checkTcpMsg(t, r.RxFrag(nil), []byte{2}, []byte("second"))
+ if m := r.RxFrag(nil); m != nil {
+ t.Fatalf("unexpected third message: %v", m)
+ }
+
+ // First message complete plus the start of the next one.
+ buf = append(append([]byte{}, b1...), b3[:3]...)
+ checkTcpMsg(t, r.RxFrag(buf), []byte{1}, []byte("first"))
+ checkTcpMsg(t, r.RxFrag(b3[3:]), []byte{3}, []byte("third"))
+ if m := r.RxFrag(nil); m != nil {
+ t.Fatalf("unexpected extra message: %v", m)
+ }
+}
+
+func TestReceiverRxBuffered(t *testing.T) {
+ udp := NewReceiver(false)
+ if m := udp.RxBuffered(); m != nil {
+ t.Fatalf("UDP receiver returned buffered message %v", m)
+ }
+
+ tcp := NewReceiver(true)
+ if m := tcp.RxBuffered(); m != nil {
+ t.Fatalf("empty TCP receiver returned message %v", m)
+ }
+
+ b1 := tcpMsg(t, []byte{1}, "", nil)
+ b2 := tcpMsg(t, []byte{2}, "", nil)
+ if m := tcp.Rx(append(append([]byte{}, b1...), b2...)); m == nil {
+ t.Fatalf("expected first message")
+ }
+ m := tcp.RxBuffered()
+ if m == nil || m.Token()[0] != 2 {
+ t.Fatalf("expected second message, got %v", m)
+ }
+ if m := tcp.RxBuffered(); m != nil {
+ t.Fatalf("unexpected message %v", m)
+ }
+}
diff --git a/nmxact/nmcoap/receiver.go b/nmxact/nmcoap/receiver.go
index c8587ee..0835535 100644
--- a/nmxact/nmcoap/receiver.go
+++ b/nmxact/nmcoap/receiver.go
@@ -57,3 +57,18 @@ func (r *Receiver) Rx(data []byte) coap.Message {
return m
}
}
+
+// RxBuffered returns the next complete message that is already buffered
+// from previously received data, or nil if there is none. Only TCP can
+// buffer messages.
+func (r *Receiver) RxBuffered() coap.Message {
+ if r.reassembler == nil {
+ return nil
+ }
+
+ tm := r.reassembler.RxFrag(nil)
+ if tm == nil {
+ return nil
+ }
+ return tm
+}