Skip to content

coap_net.c: Do not send an Empty message as a separate response - #2141

Merged
mrdeep1 merged 1 commit into
obgm:developfrom
michieldwitte:develop
Sep 2, 2026
Merged

coap_net.c: Do not send an Empty message as a separate response#2141
mrdeep1 merged 1 commit into
obgm:developfrom
michieldwitte:develop

Conversation

@michieldwitte

Copy link
Copy Markdown

A request handler that leaves the response code unset asks for no response to be sent. That is honoured for a piggybacked ACK (the token gets stripped, so an empty ACK goes out) and for a non-confirmable or reliable response (dropped in no_response()), but not for a separate response set up with coap_register_async().

There the response is turned into a CON, and a 0.00 CON was put on the wire. An Empty message is a ping (RFC 7252 4.2), not a response, and the PDU still carries the request's token, which RFC 7252 4.1 does not allow. Being confirmable, it was then retransmitted up to MAX_RETRANSMIT times, usually at a client port that had long gone away.

Drop it like the other two cases, delete it without logging it as a dropped response since the application asked for this, and document in coap_handler(3) that a separate response is not sent either.

A request handler that leaves the response code unset asks for no
response to be sent.  That is honoured for a piggybacked ACK (the token
gets stripped, so an empty ACK goes out) and for a non-confirmable or
reliable response (dropped in no_response()), but not for a separate
response set up with coap_register_async().

There the response is turned into a CON, and a 0.00 CON was put on the
wire.  An Empty message is a ping (RFC 7252 4.2), not a response, and the
PDU still carries the request's token, which RFC 7252 4.1 does not allow.
Being confirmable, it was then retransmitted up to MAX_RETRANSMIT times,
usually at a client port that had long gone away.

Drop it like the other two cases, delete it without logging it as a
dropped response since the application asked for this, and document in
coap_handler(3) that a separate response is not sent either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mrdeep1

mrdeep1 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

but not for a separate response set up with coap_register_async().

Your changes make sense as a safety net.

But, when a request handler is called, the provided response packet type is set to ACK (if request is CON), and so when the request handler returns, providing it does not set a response code, an ACK 0.0 is returned so the client knows it is a separate response and does not retry sending the request. Then when the async (separate handler) triggers, it will send the response packet with a CON 2.0x. (for NON request, the response is set to NON, which then gets dropped if there is no response code).

So, I am not sure how the request handler is returning a CON 0.0 after calling coap_register_async() to initiate a future response.

@mrdeep1

mrdeep1 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

A debug log may help here.

@michieldwitte

Copy link
Copy Markdown
Author
Real output, produced just now from libcoap's own coap-server/coap-client at 741729de (the commit before the fix), with a five-line change to the /async example so it leaves the code unset on the second call:

--- a/examples/coap-server.c
+++ b/examples/coap-server.c
@@ hnd_get_async()
   /* no request (observe) or async set up, so this is the delayed request */

+  if (query && query->length == 6 && memcmp(query->s, "silent", 6) == 0) {
+    /* DEMO: the application has nothing to answer now that the async has
+       fired, so it leaves the response code unset, as on the first call. */
+    return;
+  }
+
   coap_pdu_set_code(response, COAP_RESPONSE_CODE_CONTENT);

./coap-server -v 9 -A 127.0.0.1 -p 15683
./coap-client -v 9 -T cafe1234 -m get "coap://127.0.0.1:15683/async?silent"

Server, issue present

DEBG *  127.0.0.1:15683 <-> 127.0.0.1:49654 (if1) UDP : netif: recv   35 bytes
v:1 t:CON c:GET i:6e03 {6361666531323334} [ Uri-Path:async, Uri-Query:silent ]
DEBG call custom handler for resource 'async' (3)
DEBG    127.0.0.1:15683 <-> 127.0.0.1:49654 (if1) UDP : Async request delayed for 4.000 secs
DEBG *  127.0.0.1:15683 <-> 127.0.0.1:49654 (if1) UDP : netif: sent    4 bytes
v:1 t:ACK c:0.00 i:6e03 {} [ ]                        <-- correct: empty ACK, token stripped

DEBG Async PDU presented to app.
v:1 t:CON c:GET i:7adb {6361666531323334} [ Uri-Path:async, Uri-Query:silent ]
DEBG call custom handler for resource 'async' (3)     <-- handler #2, again sets no code
DEBG *  127.0.0.1:15683 <-> 127.0.0.1:49654 (if1) UDP : netif: sent   12 bytes
v:1 t:CON c:0.00 i:7adb {6361666531323334} [ ]        <-- the bug: 0.00 CON, 8 byte token
DEBG ** 127.0.0.1:15683 <-> ... : mid=0x7adb: added to retransmit queue (2438ms)
DEBG *  127.0.0.1:15683 <-> 127.0.0.1:49654 (if1) UDP : netif: recv    4 bytes
ALRT got RST for mid=0x7adb
v:1 t:RST c:0.00 i:7adb {} [ ]

Client side of the same exchange

This is the part worth putting in the PR — libcoap's own parser rejects the PDU libcoap just sent:

DEBG *  127.0.0.1:49654 <-> 127.0.0.1:15683 UDP : netif: recv   12 bytes
DEBG coap_pdu_parse: empty message is not empty
DEBG ***EVENT: COAP_EVENT_BAD_PACKET
WARN discard malformed PDU
DEBG *  127.0.0.1:49654 <-> 127.0.0.1:15683 UDP : netif: sent    4 bytes
v:1 t:RST c:0.00 i:7adb {} [ ]

Same run, with the fix

DEBG Async PDU presented to app.
v:1 t:CON c:GET i:84a2 {6361666531323334} [ Uri-Path:async, Uri-Query:silent ]
DEBG call custom handler for resource 'async' (3)
DEBG ***EVENT: COAP_EVENT_SERVER_SESSION_DEL          <-- nothing sent, no retransmit queue

sequenceDiagram
    participant C as Client
    participant S as libcoap server
    C->>S: CON POST /announce (mid=47ad, token=94ec…)
    Note over S: handle_request(), async == NULL<br/>handler #1: registers async, sets no code
    S-->>C: ✓ ACK 0.00, mid=47ad, token stripped (:4288)
    Note over C: receipt — stops retransmitting
    Note over S: async fires → coap_check_async() (:5405)<br/>handle_request() again, async != NULL<br/>handler #2: nothing to answer, sets no code
    Note over S: type flipped ACK→CON (:4038)<br/>request token added (:4046)<br/>no_response() covers NON/reliable only (:3674)
    S-->>C: ✗ CON 0.00, mid=47ad, token=94ec… (malformed)
    C-->>S: RST — a 0.00 CON reads as a ping
    S-->>C: retransmit × MAX_RETRANSMIT
Loading

@mrdeep1

mrdeep1 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Makes sense that when the async fires, the second call to the request handler does not update response code (unexpected).

Yes, the Libcoap library does not like empty packets that are not empty.

@mrdeep1
mrdeep1 merged commit 5e86d78 into obgm:develop Sep 2, 2026
60 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants