pubnub / dart

PubNub Dart SDK
Other
28 stars 15 forks source link

No way to catch/handle exceptions when 'No resolutions found.' #52

Closed JeffreyMcJeffFace closed 3 years ago

JeffreyMcJeffFace commented 3 years ago

pubnub version: pubnub: ^4.0.0-beta.0

While trying to implement access management (PAM), I repeatedly had an issue where I would receive an RequestFailureException. The thrown Exception occurs in subscribe_loop.dart here:

        if (resolutions == null) {
          _logger.silly('No resolutions found.');
          rethrow;
        }

In my case it was caused by trying to subscribe to a channel too quickly after my backend had applied the appropriate grants. Effectively, I was making the subscribe call before PubNub had applied the grants.

To Reproduce:

  1. Enable PAM and do not apply permissions for the client.
  2. Subscribe to a channel without a grant.
  3. Error is raised as an unhandled exception

Please provide guidance on whether I need to do something different or there is an actual bug. There are two specific items I'm trying to do:

Connection Failure Handling Know when a connection failure occurs so I can contact my backend server again to re-apply grants. It's not clear how I should implement the client to know when to handle a situation like this.

I tried wrapping subscribe call in a try/catch like this while trying to figure out what was going on, and the catch block was never hit.

    try {
      messageSubscription =
          pn?.subscribe(channels: {chatUser.channelName}, withPresence: true);
      messageSubscription?.messages.listen((message) {
        print(
            "RECEIVE for ${chatUser.channelName}: '${message.content}' with timetoken: ${message.publishedAt}");
      });
    } catch (e) {
      print("Exception Occurred while subscribing!");
    }

Subscribing Too Soon After Grant Applied When my Flutter client initiates chat, I first make a call to my backend server to generate an auth key, apply grants, and then return the key to the client where it then creates the PubNub object and subscribes. This subscribe call frequently fails.

This appears to be caused by some side of PubNub-side delay. If I put a break point on my backend code (C#) after it makes the grant call to PubNub and then allow to return API call to the Flutter client after a few moments, the Flutter client can successfully subscribe to the channel.

I'm not sure this will be an issue once deployed as the latency from an end user to our servers to PubNub and back might be long enough to prevent this from happening. However, because of inability to detect connection failure, I'm not sure how handle this situation other than putting in an explicit delay client-side before attempting the subscribe call.

are commented 3 years ago

Hello! Sorry for such a delay.

In general, an action of subscribing is decoupled from the flow of the SDK for many reasons (i.e. handling many subscriptions at once, delay because of other subscribe actions happening). This is the reason why the exception doesn't happen on the callsite of pn.subscribe method.

To prevent that, there is a mechanism in the SDK that allows to declare what exceptions are "recoverable from" (by default this mechanism is disabled, therefore if a subscription fails for any reason it throws - reasoning is that its better to find and diagnose exceptions early in the development cycle instead of masking them from the beginning).

This mechanism relies on few concepts:

In our current SDK we have a built-in "automatic request retrial" mechanism that can be enabled by passing correct configuration to the PubNub instance, but this mechanism can be extended by custom behaviour.

In your case you could use this mechanism in few steps:

I can create a Gist with the example code and you can modify it and use it in your application if you'd like. Let me know if this is something that can help you.

are commented 3 years ago

Here is the aforementioned Gist: https://gist.github.com/are/31c7a39d1798fc4a591b56b37e870e3b

Let me know if this is something that works for you or if I can help you in any other way.

are commented 3 years ago

Also, I skipped over one important detail - a lot of subscribe errors are sent through the subscription.messages stream. The default Dart behaviour is that if there is no error handler attached to the stream, the exceptions will be considered unhandled. But adding an onError: (exception) {} handler to the listen function will properly catch those exceptions and they will no longer break the whole flow.

JeffreyMcJeffFace commented 3 years ago

This is great information, thank you!

We put together an architecture for our specific use case and set the task aside until we have the available bandwidth with our backend developer. This explanation and the Gist is an excellent explanation/example. Thanks!

Brinfotech1407 commented 2 years ago

@are thanks for the gist it is helpfull , as you mentioned In our current SDK we have a built-in "automatic request retrial" mechanism that can be enabled by passing correct configuration to the PubNub instance, but this mechanism can be extended by custom behaviour. as i need a custom excpetion interface from where i can caught up 403 status exception and needs to retry that function from that common place can you help me here?

are commented 2 years ago

@Brinfotech1407 Sure! What are you having problems with in particular? What have you tried previously?

Brinfotech1407 commented 2 years ago

@are i have used your gist classes and registered the following supervisior

pubnub.supervisor.registerDiagnostic(AccessDeniedDiagnostic.handler); pubnub.supervisor.registerStrategy(AccessDeniedStrategy());

Now i have basically two methods 1) fetch history 2) publish a message

For fetching a message via history i am doing the following

            try {
              final BatchHistoryResult result =
                  await _client!.batch.fetchMessages(
                channelSet,
                count: totalMessages <= 25 ? totalMessages : 25,
                end: Timetoken(BigInt.from(endTimeToken)),
                start: startTimeToken == null
                    ? null
                    : Timetoken(BigInt.from(startTimeToken)),
                reverse: false,
              );
            } on PubNubException catch (e) {
                    if (e.message == 'Forbidden because Token is expired.') {
                        log.i('PubNubException retrying for new token , Token is expired');
                        await getPubnubAccessToken();
                        _handleHistory(); // function for retry again history
                   }
           }

And for publishing a message i am doing the following

            try {
               await _client!.publish( channelID!, json );
            } on PubNubException catch (e) {
                    if (e.message == 'Forbidden because Token is expired.') {
                        log.i('PubNubException retrying for new token , Token is expired');
                        await getPubnubAccessToken();
                         retryPublishMessage(); // function for retry again publishing a message
                   }
           }

As above both function is communicating with pubnub and i need to catch a manual exception on everyfunction i just want a common interface where we can caught a exception and retry the request after fetching a new token is there any thing we can do like that ?

are commented 2 years ago

Yes! This should be possible to do using this mechanism, however there maybe one piece missing so I'm going to work on a quick release to add this feature.

Brinfotech1407 commented 2 years ago

@are let us know if there is an update at your end.

Yes! This should be possible to do using this mechanism, however there maybe one piece missing so I'm going to work on a quick release to add this feature.

Brinfotech1407 commented 2 years ago

@are I see some connection related discussion in the latest issues, so when you are planning for this quick release?