Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash in fetchAuthValue when App is Resumed from Background #187

Open
hschk opened this issue Jan 10, 2025 · 5 comments
Open

Crash in fetchAuthValue when App is Resumed from Background #187

hschk opened this issue Jan 10, 2025 · 5 comments

Comments

@hschk
Copy link

hschk commented Jan 10, 2025

Description

Our Flutter app sometimes crashes when resuming after being in the background for a while. The issue seems to originate from the fetchAuthValue method in the Pusher library. The crash is related to a dynamic cast failure, as seen in the Crashlytics logs (provided below).

I suspect the issue might be caused by an invalid state or incorrect handling of the authorizer object during app lifecycle changes.

Environment Details

  • Pusher Library Version: 2.4.0
  • Flutter Version: 3.27.1
  • Platform: iOS

Crash Logs

Here is the relevant portion of the Crashlytics log:

Crashed: com.apple.main-thread
0  libsystem_kernel.dylib         0xc1d4 __pthread_kill + 8
1  libsystem_pthread.dylib        0x7ef8 pthread_kill + 268
2  libsystem_c.dylib              0x77ad8 abort + 128
3  libswiftCore.dylib             0x3cc1a4 swift::fatalError(unsigned int, char const*, ...) + 134
4  libswiftCore.dylib             0x3bfb44 swift::swift_dynamicCastFailure(swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*, char const*) + 78
5  pusher_channels_flutter        0x9d88 partial apply for closure #1 in SwiftPusherChannelsFlutterPlugin.fetchAuthValue(socketID:channelName:completionHandler:) + 107 (SwiftPusherChannelsFlutterPlugin.swift:107)

If any additional information is needed, please let us know.
Would appreciate any guidance or fixes from the team. Thanks!

Full Crashlytics Log:

 Crashed: com.apple.main-thread
0  libsystem_kernel.dylib         0xc1d4 __pthread_kill + 8
1  libsystem_pthread.dylib        0x7ef8 pthread_kill + 268
2  libsystem_c.dylib              0x77ad8 abort + 128
3  libswiftCore.dylib             0x3cc1a4 swift::fatalError(unsigned int, char const*, ...) + 134
4  libswiftCore.dylib             0x3cc1c4 swift::warningv(unsigned int, char const*, char*) + 30
5  libswiftCore.dylib             0x3bfb44 swift::swift_dynamicCastFailure(swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*, char const*) + 78
6  libswiftCore.dylib             0x3bfbc0 swift::_conformsToProtocol(swift::OpaqueValue const*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptorRef<swift::InProcess>, swift::TargetWitnessTable<swift::InProcess> const**) + 122
7  libswiftCore.dylib             0x3c4f68 swift_dynamicCastImpl(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*, swift::DynamicCastFlags) + 342
8  pusher_channels_flutter        0x9d88 partial apply for closure #1 in SwiftPusherChannelsFlutterPlugin.fetchAuthValue(socketID:channelName:completionHandler:) + 107 (SwiftPusherChannelsFlutterPlugin.swift:107)
9  pusher_channels_flutter        0x5bec thunk for @escaping @callee_guaranteed (@in_guaranteed Any?) -> () + 84 (<compiler-generated>:84)
10 Flutter                        0x5e2468 __54-[FlutterMethodChannel invokeMethod:arguments:result:]_block_invoke + 291 (FlutterChannels.mm:291)
11 Flutter                        0x5a178 std::_fl::__function::__func<fml::internal::CopyableLambda<flutter::PlatformMessageResponseDarwin::Complete(std::_fl::unique_ptr<fml::Mapping, std::_fl::default_delete<fml::Mapping>>)::$_0>, std::_fl::allocator<fml::internal::CopyableLambda<flutter::PlatformMessageResponseDarwin::Complete(std::_fl::unique_ptr<fml::Mapping, std::_fl::default_delete<fml::Mapping>>)::$_0>>, void ()>::operator()() + 24 (platform_message_response_darwin.mm:24)
12 Flutter                        0x85c9c fml::MessageLoopImpl::FlushTasks(fml::FlushType) + 128 (message_loop_impl.cc:128)
13 Flutter                        0x89314 fml::MessageLoopDarwin::OnTimerFire(__CFRunLoopTimer*, fml::MessageLoopDarwin*) + 86 (message_loop_darwin.mm:86)
14 CoreFoundation                 0xb4894 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 32
15 CoreFoundation                 0xb4538 __CFRunLoopDoTimer + 1012
16 CoreFoundation                 0xb408c __CFRunLoopDoTimers + 288
17 CoreFoundation                 0x533b4 __CFRunLoopRun + 1856
18 CoreFoundation                 0x52830 CFRunLoopRunSpecific + 588
19 GraphicsServices               0x11c4 GSEventRunModal + 164
20 UIKitCore                      0x3d2eb0 -[UIApplication _run] + 816
21 UIKitCore                      0x4815b4 UIApplicationMain + 340
22 UIKitCore                      0x7bbfa8 block_destroy_helper.23 + 10212
23 Runner                         0x88cc main + 4343269580 (AppDelegate.swift:4343269580)
@hschk
Copy link
Author

hschk commented Jan 11, 2025

I’m not sure if this is related, but when I reopen the app after it has been in the background, I see a series of connection state change logs like this:

flutter: Connection state changed: DISCONNECTED
flutter: Connection state changed: RECONNECTING
flutter: Connection state changed: CONNECTING
flutter: Connection state changed: DISCONNECTED
flutter: Connection state changed: RECONNECTING
flutter: Connection state changed: DISCONNECTED
flutter: Connection state changed: RECONNECTING
flutter: Connection state changed: DISCONNECTED
flutter: Connection state changed: RECONNECTING
flutter: Connection state changed: CONNECTING
flutter: Connection state changed: CONNECTED

Is this expected behavior? As I mentioned, this might not be an issue or even related to the crashes, but I thought it was worth noting while troubleshooting.

@kadersaka
Copy link

which version of the pusher channel flutter library r u using?

@hschk
Copy link
Author

hschk commented Jan 11, 2025

When I reported this issue, I was using version 2.4.0. However, the same behavior occurs with version 2.5.0.

@kadersaka
Copy link

kadersaka commented Jan 11, 2025

please paste here ur pusher init code and functions. u can also directly message me on kadersaka[at]gmail.com so we can go for a live working session

am suspecting the issue is related to your onAuthorizer code.
image

@hschk
Copy link
Author

hschk commented Jan 12, 2025

Thank you for your response and for offering to help!

I also suspected that the issue might lie within my onAuthorizer code. However, after reviewing our server logs, it appears that our Pusher auth endpoint was returning the expected response with a 200 status code and the correct authentication object at the time of the last crash.

To provide more context, here is my PusherService class implementation:


import 'dart:async';
import 'dart:convert';

import 'package:bridge/business_logic/utilities/locator.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:logger/logger.dart';
import 'package:pusher_channels_flutter/pusher_channels_flutter.dart';
import 'package:synchronized/synchronized.dart';
import 'package:http/http.dart' as http;

class PusherService {
  // Singleton pattern
  static final PusherService _instance = PusherService._internal();

  factory PusherService() => _instance;

  PusherService._internal();

  final Lock _lock = Lock();
  PusherChannelsFlutter? _pusher;
  bool _isConnected = false;

  final String apiKey = dotenv.env['PUSHER_KEY'] ?? '';
  final String cluster = dotenv.env['PUSHER_CLUSTER'] ?? '';
  final String authEndpoint = dotenv.env['PUSHER_AUTH_ENDPOINT'] ?? '';

  final Logger _logger = getIt<Logger>();

  /// Initializes the Pusher service.
  Future<void> initPusher() async {
    await _lock.synchronized(() async {
      if (_isConnected) return;

      _pusher = PusherChannelsFlutter.getInstance();
      try {
        await _pusher!.init(
          apiKey: apiKey,
          cluster: cluster,
          onAuthorizer: _onAuthorizer,
          onConnectionStateChange: _onConnectionStateChange,
          onError: _onError,
          onSubscriptionSucceeded: _onSubscriptionSucceeded,
          onEvent: _onEvent,
          onSubscriptionError: _onSubscriptionError,
        );
        await _pusher!.connect();
        _isConnected = true;
        _logger.i('Pusher connected successfully.');
      } catch (e, stackTrace) {
        _logger.e('Failed to initialize Pusher',
            error: e, stackTrace: stackTrace);
        FirebaseCrashlytics.instance.recordError(e, stackTrace);
      }
    });
  }

  /// Disconnects from the Pusher service.
  Future<void> disconnect() async {
    if (_isConnected) {
      try {
        await _pusher?.disconnect();
        _isConnected = false;
        _logger.i('Pusher disconnected.');
      } catch (e, stackTrace) {
        _logger.e('Failed to disconnect Pusher',
            error: e, stackTrace: stackTrace);
      }
    }
  }

  /// Subscribes to a Pusher channel.
  Future<PusherChannel> subscribeToChannel(
    String channelName, {
    bool isPrivate = true,
    void Function(PusherEvent event)? onEvent,
  }) async {
    await initPusher();

    final fullChannelName = isPrivate ? 'private-$channelName' : channelName;

    try {
      return await _pusher!.subscribe(
        channelName: fullChannelName,
        onEvent: onEvent != null
            ? (dynamic event) =>
                onEvent(event as PusherEvent) // Typecast auf PusherEvent
            : null,
      );
    } catch (e, stackTrace) {
      _logger.e('Failed to subscribe to channel $fullChannelName',
          error: e, stackTrace: stackTrace);
      rethrow;
    }
  }

  /// Handles authorization for private channels.
  Future<Map<String, String>?> _onAuthorizer(
    String channelName,
    String socketId,
    dynamic options,
  ) async {
    final token = await FirebaseAuth.instance.currentUser?.getIdToken();
    if (token == null) {
      _logger.w('Authorization failed: No token available.');
      return null;
    }

    try {
      final response = await http.post(
        Uri.parse(authEndpoint),
        headers: {
          'Authorization': 'Bearer $token',
        },
        body: {
          'socket_id': socketId,
          'channel_name': channelName,
        },
      );

      if (response.statusCode == 200) {
        return Map<String, String>.from(jsonDecode(response.body));
      } else {
        _logger.w(
          'Authorization failed with status code ${response.statusCode}: ${response.body}',
        );
      }
    } catch (e, stackTrace) {
      _logger.e('Authorization error', error: e, stackTrace: stackTrace);
    }
    return null;
  }

  // Callbacks for Pusher events
  void _onConnectionStateChange(String currentState, String? previousState) {
    _logger.i('Connection state changed: $currentState (from $previousState)');
  }

  void _onError(String message, int? code, dynamic e) {
    _logger.e('Pusher error: $message (Code: $code)', error: e);
  }

  void _onSubscriptionSucceeded(String channelName, dynamic data) {
    _logger.i('Subscription succeeded: $channelName');
  }

  void _onEvent(PusherEvent event) {
    _logger.d('Event received: ${event.data}');
  }

  void _onSubscriptionError(String message, dynamic e) {
    _logger.e('Subscription error: $message', error: e);
  }
}

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

No branches or pull requests

2 participants