felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.79k stars 3.39k forks source link

How to call logout in API service class when API throws 401 error #3515

Closed georgek1991 closed 1 year ago

georgek1991 commented 2 years ago

I am working with bloc in my project which is working great.

I have a API service class which uses Dio for making API requests. I am using an Interceptors for handling error. What I am trying to achieve is whenever API throws 401 error I want to logout the user and show login page. I will also be invalidating the token.

So my question is, If I want to logout the user as soon as 401 occur ie. from API service class do I call logout event which is defined in the Authentication Bloc. To do so I will have to have instance of Authentication Bloc in API Service class. Is this approach correct? or is there any other simpler ways to achieve this.

class APIService{
...
      onError: (DioError error, handler) async {
          if (error.response?.statusCode == 401){
              //invalidate the token
             //Do I call the logout event of authentication bloc here?
          }
      }
...
}
vishalpatel1327 commented 2 years ago

that's not bug but a Question

I also have the same question about that.

can anyone guide this scenario to us?

georgek1991 commented 2 years ago

@felangel Can you please help.

c0pp1ce commented 2 years ago

I am unsure what the best way to handle it would be, here are two ideas anyway (Assuming this failure can occur in multiple blocs)

Loosely Following Clean Architecture In the data layer (e.g. your APIService) you could throw some kind of server exception which contains the status code. Then in your repository you would catch and handle the exception (e.g. return a failure object). Let your blocs emit a 401 error state whenever this happens. You could then add a AuthFailed event to your auth bloc. Based on this you could instantly trigger the logout or emit some error state and e.g. display some dialog to the user before the logout. The downside I see with this is that you need to take care of it for every bloc/repository using the api service (convert 401 exception to failure object to state & add an even to the auth bloc).

Streams and DI (not too sure how streams work in Dart) Add a 401-error stream to your API service. Your auth bloc can listen to this stream and act accordingly. Inject the instance of the service into any repository that needs it. This way you dont need explicit 401-failure states in every bloc that uses the API service.

georgek1991 commented 2 years ago

I am unsure what the best way to handle it would be, here are two ideas anyway (Assuming this failure can occur in multiple blocs)

Loosely Following Clean Architecture In the data layer (e.g. your APIService) you could throw some kind of server exception which contains the status code. Then in your repository you would catch and handle the exception (e.g. return a failure object). Let your blocs emit a 401 error state whenever this happens. You could then add a AuthFailed event to your auth bloc. Based on this you could instantly trigger the logout or emit some error state and e.g. display some dialog to the user before the logout. The downside I see with this is that you need to take care of it for every bloc/repository using the api service (convert 401 exception to failure object to state & add an even to the auth bloc).

Streams and DI (not too sure how streams work in Dart) Add a 401-error stream to your API service. Your auth bloc can listen to this stream and act accordingly. Inject the instance of the service into any repository that needs it. This way you dont need explicit 401-failure states in every bloc that uses the API service.

Can you please give a simple example for 2nd idea?

c0pp1ce commented 2 years ago

This kind of is what I had in mind. Again, I am not used to streams in dart (and bloc in general) - this can probably be improved quite a bit (e.g. a way to close the stream is missing). The idea is based on the documentation for bloc to bloc communication ( https://bloclibrary.dev/#/architecture?id=connecting-blocs-through-domain )

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';

/// Your API service. Provide the same instance to every repository.
/// Stream based on https://dart.dev/articles/libraries/creating-streams#using-a-streamcontroller
class APIService {
  APIService() {
    controller = StreamController(
      onListen: _onListen,
      onPause: _onPause,
      onResume: _onResume,
      onCancel: _onCancel,
    );
  }

  late final StreamController<UnauthenticatedFailure> controller;

  bool _streamHasListeners = false;
  bool _streamIsPaused = false;

  Stream<UnauthenticatedFailure> unauthenticatedFailures() {
    return controller.stream;
  }

  void _onError(int statusCode) {
    if (statusCode == 401 && _streamHasListeners && !_streamIsPaused) {
      controller.add(UnauthenticatedFailure());
    }
  }

  void _onListen() {
    _streamHasListeners = true;
  }

  void _onPause() {
    _streamIsPaused = true;
  }

  void _onResume() {
    _streamIsPaused = false;
  }

  void _onCancel() {
    _streamHasListeners = false;
  }
}

/// Your Failure class used by the stream & auth bloc.
class UnauthenticatedFailure {}

/// The auth bloc.
class AuthState {}

class AuthEvent {}

class AuthenticatedEvent extends AuthEvent {} // Triggered when the user logs in.

class AuthenticationRevokedEvent extends AuthEvent {}

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc({
    required this.apiService,
  }) : super(AuthState()) {
    on<AuthEvent>((event, emit) async {
      if (event is AuthenticatedEvent) {
        // Log in the user.
        // Fire some Authenticated state.
        // ...
        // Listen to the API service to discover if you need to change to unauthenticated state.
        // Use forEach to directly map the stream to states.
        await emit.onEach(apiService.unauthenticatedFailures(), onData: (_) {
          print("401 failure!");
          add(AuthenticationRevokedEvent());
        });
      } else if (event is AuthenticationRevokedEvent) {
        print("Auth revoked!");
        await Future.delayed(const Duration(seconds: 3)); // Emulate logout.
        print("Logged out!");
      }
      // Cancels stream subscription once the user is not authenticated anymore.
    }, transformer: restartable());
  }

  final APIService apiService;
}

// Basic usage example.
void main() async {
  // Create the service.
  final APIService apiService = APIService();
  // Use the service in your blocs.
  final AuthBloc authBloc = AuthBloc(apiService: apiService);
  // Trigger listening to the stream.
  authBloc.add(AuthenticatedEvent());

  // Emulate api calls that add the failure. Timer to show that only the first
  // failure will trigger the auth revoked event aka. keeping the state clean
  // even if there are many api calls failing in a short time.
  await Future.delayed(const Duration(seconds: 2));
  int counter = 0;
  final timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
    if (counter == 5) timer.cancel();

    apiService.controller.add(UnauthenticatedFailure());
    counter++;
  });
}
felangel commented 1 year ago

I recommend using an interceptor like package:fresh_dio to handle the 401 and expose a stream of authentication states which the bloc can react to very similar to how the firebase_login example is structured.

Hope that helps, closing for now but feel free to comment with any follow up questions/comments and I'm happy to continue the conversation 👍