Skip to content

Commit ece74f6

Browse files
mohitpubnubMohit Tejanipubnub-release-bot
authored
fix for subscription to continue after exception from listener callback (#266)
* added more tests to improve test coverage in SDK * PubNub SDK v8.0.2 release. --------- Co-authored-by: Mohit Tejani <[email protected]> Co-authored-by: PubNub Release Bot <[email protected]>
1 parent 8e76683 commit ece74f6

File tree

14 files changed

+4316
-64
lines changed

14 files changed

+4316
-64
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ src/PNEventEngine/obj/*
184184
src/PNEventEngineUWP/bin/*
185185
src/PNEventEngineUWP/obj/*
186186

187+
CLAUDE.md
188+
.claude/*
189+
.cursor/*
190+
187191
# GitHub Actions #
188192
##################
189193
.github/.release

.pubnub.yml

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
name: c-sharp
2-
version: "8.0.1"
2+
version: "8.0.2"
33
schema: 1
44
scm: github.com/pubnub/c-sharp
55
changelog:
6+
- date: 2025-11-17
7+
version: v8.0.2
8+
changes:
9+
- type: bug
10+
text: "Fixes issue of subscription loop breaking when a listener callback throws exception."
611
- date: 2025-11-04
712
version: v8.0.1
813
changes:
@@ -957,7 +962,7 @@ features:
957962
- QUERY-PARAM
958963
supported-platforms:
959964
-
960-
version: Pubnub 'C#' 8.0.1
965+
version: Pubnub 'C#' 8.0.2
961966
platforms:
962967
- Windows 10 and up
963968
- Windows Server 2008 and up
@@ -968,7 +973,7 @@ supported-platforms:
968973
- .Net Framework 4.6.1+
969974
- .Net Framework 6.0
970975
-
971-
version: PubnubPCL 'C#' 8.0.1
976+
version: PubnubPCL 'C#' 8.0.2
972977
platforms:
973978
- Xamarin.Android
974979
- Xamarin.iOS
@@ -988,7 +993,7 @@ supported-platforms:
988993
- .Net Core
989994
- .Net 6.0
990995
-
991-
version: PubnubUWP 'C#' 8.0.1
996+
version: PubnubUWP 'C#' 8.0.2
992997
platforms:
993998
- Windows Phone 10
994999
- Universal Windows Apps
@@ -1012,7 +1017,7 @@ sdks:
10121017
distribution-type: source
10131018
distribution-repository: GitHub
10141019
package-name: Pubnub
1015-
location: https://github.com/pubnub/c-sharp/releases/tag/v8.0.1.0
1020+
location: https://github.com/pubnub/c-sharp/releases/tag/v8.0.2
10161021
requires:
10171022
-
10181023
name: ".Net"
@@ -1295,7 +1300,7 @@ sdks:
12951300
distribution-type: source
12961301
distribution-repository: GitHub
12971302
package-name: PubNubPCL
1298-
location: https://github.com/pubnub/c-sharp/releases/tag/v8.0.1.0
1303+
location: https://github.com/pubnub/c-sharp/releases/tag/v8.0.2
12991304
requires:
13001305
-
13011306
name: ".Net Core"
@@ -1654,7 +1659,7 @@ sdks:
16541659
distribution-type: source
16551660
distribution-repository: GitHub
16561661
package-name: PubnubUWP
1657-
location: https://github.com/pubnub/c-sharp/releases/tag/v8.0.1.0
1662+
location: https://github.com/pubnub/c-sharp/releases/tag/v8.0.2
16581663
requires:
16591664
-
16601665
name: "Universal Windows Platform Development"

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
v8.0.2 - November 17 2025
2+
-----------------------------
3+
- Fixed: fixes issue of subscription loop breaking when a listener callback throws exception.
4+
15
v8.0.1 - November 04 2025
26
-----------------------------
37
- Modified: removed `limit` value clamp for HereNow API. Server will perform validation for limit parameter value.

src/Api/PubnubApi/EndPoint/PubSub/SubscribeEndpoint.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,29 @@ private void MessageEmitter<T>(Pubnub pubnubInstance, PNMessageResult<T> message
216216
{
217217
foreach (var listener in SubscribeListenerList)
218218
{
219-
listener?.Message(pubnubInstance, messageResult);
219+
try
220+
{
221+
listener?.Message(pubnubInstance, messageResult);
222+
}
223+
catch (Exception ex)
224+
{
225+
config.Logger?.Error($"error during event handler function, {ex.Message}");
226+
}
220227
}
221228
}
222229

223230
private void StatusEmitter(Pubnub pubnubInstance, PNStatus status)
224231
{
225232
foreach (var listener in SubscribeListenerList)
226233
{
227-
listener?.Status(pubnubInstance, status);
234+
try
235+
{
236+
listener?.Status(pubnubInstance, status);
237+
}
238+
catch (Exception ex)
239+
{
240+
config.Logger?.Error($"error during event handler function, {ex.Message}");
241+
}
228242
}
229243
}
230244

src/Api/PubnubApi/EventEngine/Common/EventEmitter.cs

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,32 @@ public EventEmitter(PNConfiguration configuration, List<SubscribeCallback> liste
3131
channelListenersMap = new Dictionary<string, List<SubscribeCallback>>();
3232
}
3333

34+
/// <summary>
35+
/// Safely invokes a listener callback with exception handling to prevent listener exceptions from affecting SDK operations.
36+
/// </summary>
37+
/// <param name="listenerAction">The listener callback action to invoke.</param>
38+
/// <param name="eventType">The type of event being processed (for logging purposes).</param>
39+
/// <remarks>
40+
/// This method ensures that exceptions thrown by user-provided listener callbacks do not crash the SDK
41+
/// or prevent other listeners from receiving events. All exceptions are logged at Warning level.
42+
/// </remarks>
43+
private void SafeInvokeListener(Action listenerAction, string eventType)
44+
{
45+
if (listenerAction == null)
46+
{
47+
return;
48+
}
49+
50+
try
51+
{
52+
listenerAction.Invoke();
53+
}
54+
catch (Exception ex)
55+
{
56+
configuration?.Logger?.Warn($"Exception in listener callback execution for {eventType}: {ex.Message}");
57+
}
58+
}
59+
3460
private TimetokenMetadata GetTimetokenMetadata(object t)
3561
{
3662
Dictionary<string, object> ttOriginMetaData = jsonLibrary.ConvertToDictionaryObject(t);
@@ -214,22 +240,22 @@ public void EmitEvent<T>(object e)
214240
};
215241
foreach (var listener in listeners)
216242
{
217-
listener?.Signal(instance, signalMessage);
243+
SafeInvokeListener(() => listener?.Signal(instance, signalMessage), "Signal");
218244
}
219245

220246
if (!string.IsNullOrEmpty(signalMessage.Channel) && channelListenersMap.ContainsKey(signalMessage.Channel))
221247
{
222248
foreach (var l in channelListenersMap[signalMessage.Channel])
223249
{
224-
l?.Signal(instance, signalMessage);
250+
SafeInvokeListener(() => l?.Signal(instance, signalMessage), "Signal");
225251
}
226252
}
227253

228254
if (!string.IsNullOrEmpty(signalMessage.Subscription) && channelGroupListenersMap.ContainsKey(signalMessage.Subscription))
229255
{
230256
foreach (var l in channelGroupListenersMap[signalMessage.Subscription])
231257
{
232-
l?.Signal(instance, signalMessage);
258+
SafeInvokeListener(() => l?.Signal(instance, signalMessage), "Signal");
233259
}
234260
}
235261
}
@@ -244,22 +270,22 @@ public void EmitEvent<T>(object e)
244270
{
245271
foreach (var listener in listeners)
246272
{
247-
listener?.ObjectEvent(instance, objectApiEvent);
273+
SafeInvokeListener(() => listener?.ObjectEvent(instance, objectApiEvent), "ObjectEvent");
248274
}
249275

250276
if (!string.IsNullOrEmpty(objectApiEvent.Channel) && channelListenersMap.ContainsKey(objectApiEvent.Channel))
251277
{
252278
foreach (var l in channelListenersMap[objectApiEvent.Channel])
253279
{
254-
l?.ObjectEvent(instance, objectApiEvent);
280+
SafeInvokeListener(() => l?.ObjectEvent(instance, objectApiEvent), "ObjectEvent");
255281
}
256282
}
257283

258284
if (!string.IsNullOrEmpty(objectApiEvent.Subscription) && channelGroupListenersMap.ContainsKey(objectApiEvent.Subscription))
259285
{
260286
foreach (var l in channelGroupListenersMap[objectApiEvent.Subscription])
261287
{
262-
l?.ObjectEvent(instance, objectApiEvent);
288+
SafeInvokeListener(() => l?.ObjectEvent(instance, objectApiEvent), "ObjectEvent");
263289
}
264290
}
265291
}
@@ -274,22 +300,22 @@ public void EmitEvent<T>(object e)
274300
{
275301
foreach (var listener in listeners)
276302
{
277-
listener?.MessageAction(instance, messageActionEvent);
303+
SafeInvokeListener(() => listener?.MessageAction(instance, messageActionEvent), "MessageAction");
278304
}
279305

280306
if (!string.IsNullOrEmpty(messageActionEvent.Channel) && channelListenersMap.ContainsKey(messageActionEvent.Channel))
281307
{
282308
foreach (var l in channelListenersMap[messageActionEvent.Channel])
283309
{
284-
l?.MessageAction(instance, messageActionEvent);
310+
SafeInvokeListener(() => l?.MessageAction(instance, messageActionEvent), "MessageAction");
285311
}
286312
}
287313

288314
if (!string.IsNullOrEmpty(messageActionEvent.Subscription) && channelGroupListenersMap.ContainsKey(messageActionEvent.Subscription))
289315
{
290316
foreach (var l in channelGroupListenersMap[messageActionEvent.Subscription])
291317
{
292-
l?.MessageAction(instance, messageActionEvent);
318+
SafeInvokeListener(() => l?.MessageAction(instance, messageActionEvent), "MessageAction");
293319
}
294320
}
295321
}
@@ -340,22 +366,22 @@ public void EmitEvent<T>(object e)
340366

341367
foreach (var listener in listeners)
342368
{
343-
listener?.File(instance, fileMessage);
369+
SafeInvokeListener(() => listener?.File(instance, fileMessage), "File");
344370
}
345371

346372
if (!string.IsNullOrEmpty(fileMessage.Channel) && channelListenersMap.ContainsKey(fileMessage.Channel))
347373
{
348374
foreach (var l in channelListenersMap[fileMessage.Channel])
349375
{
350-
l?.File(instance, fileMessage);
376+
SafeInvokeListener(() => l?.File(instance, fileMessage), "File");
351377
}
352378
}
353379

354380
if (!string.IsNullOrEmpty(fileMessage.Subscription) && channelGroupListenersMap.ContainsKey(fileMessage.Subscription))
355381
{
356382
foreach (var l in channelGroupListenersMap[fileMessage.Subscription])
357383
{
358-
l?.File(instance, fileMessage);
384+
SafeInvokeListener(() => l?.File(instance, fileMessage), "File");
359385
}
360386
}
361387
}
@@ -372,22 +398,22 @@ public void EmitEvent<T>(object e)
372398
{
373399
foreach (var listener in listeners)
374400
{
375-
listener?.Presence(instance, presenceEvent);
401+
SafeInvokeListener(() => listener?.Presence(instance, presenceEvent), "Presence");
376402
}
377403

378404
if (!string.IsNullOrEmpty(presenceEvent.Channel) && channelListenersMap.ContainsKey(presenceEvent.Channel))
379405
{
380406
foreach (var l in channelListenersMap[presenceEvent.Channel])
381407
{
382-
l?.Presence(instance, presenceEvent);
408+
SafeInvokeListener(() => l?.Presence(instance, presenceEvent), "Presence");
383409
}
384410
}
385411

386412
if (!string.IsNullOrEmpty(presenceEvent.Subscription) && channelGroupListenersMap.ContainsKey(presenceEvent.Subscription))
387413
{
388414
foreach (var l in channelGroupListenersMap[presenceEvent.Subscription])
389415
{
390-
l?.Presence(instance, presenceEvent);
416+
SafeInvokeListener(() => l?.Presence(instance, presenceEvent), "Presence");
391417
}
392418
}
393419
}
@@ -397,37 +423,29 @@ public void EmitEvent<T>(object e)
397423
jsonFields.Add("customMessageType", eventData.CustomMessageType);
398424
ResponseBuilder responseBuilder =new ResponseBuilder(configuration, jsonLibrary);
399425
PNMessageResult<T> userMessage = responseBuilder.GetEventResultObject<PNMessageResult<T>>(jsonFields);
400-
try
426+
if (userMessage != null)
401427
{
402-
if (userMessage != null)
428+
foreach (var listener in listeners)
403429
{
404-
foreach (var listener in listeners)
405-
{
406-
listener?.Message(instance, userMessage);
407-
}
430+
SafeInvokeListener(() => listener?.Message(instance, userMessage), "Message");
431+
}
408432

409-
if (!string.IsNullOrEmpty(userMessage.Channel) && channelListenersMap.ContainsKey(userMessage.Channel))
433+
if (!string.IsNullOrEmpty(userMessage.Channel) && channelListenersMap.ContainsKey(userMessage.Channel))
434+
{
435+
foreach (var l in channelListenersMap[userMessage.Channel])
410436
{
411-
foreach (var l in channelListenersMap[userMessage.Channel])
412-
{
413-
l?.Message(instance, userMessage);
414-
}
437+
SafeInvokeListener(() => l?.Message(instance, userMessage), "Message");
415438
}
439+
}
416440

417-
if (!string.IsNullOrEmpty(userMessage.Subscription) && channelGroupListenersMap.ContainsKey(userMessage.Subscription))
441+
if (!string.IsNullOrEmpty(userMessage.Subscription) && channelGroupListenersMap.ContainsKey(userMessage.Subscription))
442+
{
443+
foreach (var l in channelGroupListenersMap[userMessage.Subscription])
418444
{
419-
foreach (var l in channelGroupListenersMap[userMessage.Subscription])
420-
{
421-
l?.Message(instance, userMessage);
422-
}
445+
SafeInvokeListener(() => l?.Message(instance, userMessage), "Message");
423446
}
424447
}
425448
}
426-
catch (Exception ex)
427-
{
428-
configuration.Logger?.Error(
429-
$"Listener call back execution encounters error: {ex.Message}\n{ex?.StackTrace}");
430-
}
431449
}
432450

433451
break;

src/Api/PubnubApi/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
[assembly: AssemblyProduct("Pubnub C# SDK")]
1212
[assembly: AssemblyCopyright("Copyright © 2021")]
1313
[assembly: AssemblyTrademark("")]
14-
[assembly: AssemblyVersion("8.0.1.0")]
15-
[assembly: AssemblyFileVersion("8.0.1.0")]
14+
[assembly: AssemblyVersion("8.0.2")]
15+
[assembly: AssemblyFileVersion("8.0.2")]
1616
// Setting ComVisible to false makes the types in this assembly not visible
1717
// to COM components. If you need to access a type in this assembly from
1818
// COM, set the ComVisible attribute to true on that type.

src/Api/PubnubApi/PubnubApi.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414

1515
<PropertyGroup>
1616
<PackageId>Pubnub</PackageId>
17-
<PackageVersion>8.0.1.0</PackageVersion>
17+
<PackageVersion>8.0.2</PackageVersion>
1818
<Title>PubNub C# .NET - Web Data Push API</Title>
1919
<Authors>Pandu Masabathula</Authors>
2020
<Owners>PubNub</Owners>
2121
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
2222
<PackageIconUrl>http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png</PackageIconUrl>
2323
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
2424
<RepositoryUrl>https://github.com/pubnub/c-sharp/</RepositoryUrl>
25-
<PackageReleaseNotes>Removed `limit` value clamp for HereNow API. Server will perform validation for limit parameter value.</PackageReleaseNotes>
25+
<PackageReleaseNotes>Fixes issue of subscription loop breaking when a listener callback throws exception.</PackageReleaseNotes>
2626
<PackageTags>Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing</PackageTags>
2727
<!--<Summary>PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously</Summary>-->
2828
<Description>PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously</Description>

0 commit comments

Comments
 (0)