Commit ebbc0a655d for asterisk.org
commit ebbc0a655df24676d88db3d16839358035a3f4cc
Author: rikrobson <richard@rikrobson.co.uk>
Date: Tue Sep 29 17:32:43 2026 +0000
res_pjsip_pubsub: Persist the CSeq of the NOTIFY that answers a refresh.
Since pjproject 2.13 the NOTIFY generated while handling a SUBSCRIBE
refresh is cached and sent after the 200 OK, and the dialog's local CSeq
only advances when it is sent. Both persistence updates made for the
refresh therefore store the CSeq from before that NOTIFY. If the
subscription is recreated from persistence before another NOTIFY is
sent, its first NOTIFY reuses that CSeq, which subscribers reject (e.g.
with 500), and the subscription is then terminated.
The subscription is now persisted again from a task on its serializer,
which runs after the cached NOTIFY has been sent.
Assisted-by: Claude Opus 5.5
Resolves: #2189
diff --git a/res/res_pjsip_pubsub.c b/res/res_pjsip_pubsub.c
index 9dd999284d..51f5a881e6 100644
--- a/res/res_pjsip_pubsub.c
+++ b/res/res_pjsip_pubsub.c
@@ -4245,6 +4245,32 @@ static int pubsub_on_expiration_timeout(void *userdata)
return 0;
}
+#ifdef HAVE_PJSIP_EVSUB_PENDING_NOTIFY
+/*!
+ * \brief Persist the CSeq of the NOTIFY that answered a refresh
+ *
+ * pjproject sends that NOTIFY after its reply to the SUBSCRIBE, and only then
+ * advances the dialog's local CSeq, so the persistence updates made while
+ * handling the refresh store the CSeq from before it. A subscription
+ * recreated from that record would reuse the NOTIFY's CSeq. This task runs
+ * on the subscription's serializer after the NOTIFY has been sent.
+ */
+static int serialized_persist_refresh_notify(void *userdata)
+{
+ struct sip_subscription_tree *sub_tree = userdata;
+ pjsip_dialog *dlg = sub_tree->dlg;
+
+ if (dlg) {
+ pjsip_dlg_inc_lock(dlg);
+ subscription_persistence_update(sub_tree, NULL, SUBSCRIPTION_PERSISTENCE_SEND_REQUEST);
+ pjsip_dlg_dec_lock(dlg);
+ }
+ ao2_cleanup(sub_tree);
+
+ return 0;
+}
+#endif
+
static int serialized_pubsub_on_refresh_timeout(void *userdata)
{
struct sip_subscription_tree *sub_tree = userdata;
@@ -4427,6 +4453,20 @@ static void pubsub_on_rx_refresh(pjsip_evsub *evsub, pjsip_rx_data *rdata,
looks for the NOTIFY to be sent from this function and caches it to send after it
auto-replies to the SUBSCRIBE. */
pubsub_on_refresh_timeout(sub_tree);
+ /* The NOTIFY above has not been sent yet: pjproject only cached it, and the
+ dialog's local CSeq advances when it is actually sent. So the persistence
+ updates made so far for this refresh store the CSeq from before it.
+
+ This function runs within a task on the subscription's serializer (in-dialog
+ requests are dispatched to the dialog's serializer, set to sub_tree->serializer
+ in subscription_setup_dialog). After it returns, and still within that same
+ task, pjproject sends the 200 OK to the SUBSCRIBE and then the cached NOTIFY.
+ A serializer runs one task at a time in order, so a task pushed here runs only
+ after that NOTIFY has been sent, and persists the CSeq it used. */
+ if (ast_sip_push_task(sub_tree->serializer, serialized_persist_refresh_notify, ao2_bump(sub_tree))) {
+ ast_log(LOG_WARNING, "Failed to push task to persist the refresh NOTIFY's CSeq.\n");
+ ao2_ref(sub_tree, -1);
+ }
#else
if (ast_sip_push_task(sub_tree->serializer, serialized_pubsub_on_refresh_timeout, ao2_bump(sub_tree))) {
/* If we can't push the NOTIFY refreshing task...we'll just go with it. */