Skip to content

Conversation

@dab246
Copy link
Member

@dab246 dab246 commented Oct 7, 2025

Issue

#4081

Analysis and Solutions

Error handling

  • When encountering a network connection error (NoNetworkError), display the message "No internet connection" to the user.
  • When encountering a 401 error, perform a token refresh (refreshToken).
    • If refreshToken throws an exception, display a message to the user in the format:
      "Message [CodeError1 - CodeError2]".
    • Otherwise, if refreshToken succeeds but returns an empty token, automatically log in again using the current user's information.

Demo

  • Case: Auto re-login with user email on 401 error for mobile
auto-refresh.webm
  • Case: User logs out
logout.webm

@github-actions
Copy link

github-actions bot commented Oct 7, 2025

This PR has been deployed to https://linagora.github.io/tmail-flutter/4088.

@chibenwa
Copy link
Member

chibenwa commented Oct 7, 2025

Auto re-login with user email on 401 error for mobile

It could be great to display somehow an explanation of what happens to the user.
I think it could reduce the friction

Copy link
Member

@chibenwa chibenwa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TOO LONG, DID NOT READ

1000+ LOC changeset is a lot, even more for a simple bugfic.

I see a lot of duplicated try catch.

This is IMO symptomatic. I would have expected either a generic behaviour of the HTTP library, or an abstraction lying in tmail-flutter or even better in jmap-dart to help us significantly reduce the boiler plate code.

@dab246
Copy link
Member Author

dab246 commented Oct 8, 2025

TOO LONG, DID NOT READ

1000+ LOC changeset is a lot, even more for a simple bugfic.

I see a lot of duplicated try catch.

This is IMO symptomatic. I would have expected either a generic behaviour of the HTTP library, or an abstraction lying in tmail-flutter or even better in jmap-dart to help us significantly reduce the boiler plate code.

IMO, this is not a simple bugfix.
Let me explain why there are so many duplicated try-catch blocks.

  • I have separated the commits clearly for each specific bugfix.

  • The reason for the 1000+ changed lines is due to the last commit, which addresses the following runtime error related to the Future.catchError method:

Invalid argument(s) (onError): The error handler of Future.catchError must return a value of the future's type

To fix this issue properly, we needed to update all related data sources. Since we are following Clean Architecture, with clear separation and abstraction across layers, multiple features depend on different data sources. Therefore, we have to apply try/catch in multiple places. However, the exception handling logic itself is centralized in a single common class, ensuring consistency across the app.

@dab246
Copy link
Member Author

dab246 commented Oct 8, 2025

Auto re-login with user email on 401 error for mobile

It could be great to display somehow an explanation of what happens to the user. I think it could reduce the friction

If you have any suggestions, please share them with us.
It would be even better if you could provide a mockup or a video.

@dab246 dab246 requested a review from chibenwa October 8, 2025 03:10
@dab246 dab246 force-pushed the bugfix/tf-4081-mobile-user-was-logged-out-again branch from f660f2b to 10f30f4 Compare October 22, 2025 02:59
@hoangdat
Copy link
Member

  • please rebase it with master branch @dab246

@dab246
Copy link
Member Author

dab246 commented Nov 12, 2025

  • please rebase it with master branch @dab246

Done

logError('RemoteExceptionThrower::throwException():isNetworkConnectionAvailable');
final realtimeNetworkConnectionStatus = await networkConnectionController?.hasInternetConnection();
if (realtimeNetworkConnectionStatus == false) {
logError('RemoteExceptionThrower::throwException(): No realtime network connection');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really true, right?

Let's say:

  • I make a request
  • This request throw a server error (and not a network error because at this time I'm connected)
  • During the throw, we check if I've an internet connection but I was on a train, and suddenly I lost my internet connection.
  • At the end, we'll throw a "NoNetworkError" instead of a "ServerError"

From dio don't we have a message saying "connection timeout" / "connexion reset" or something like that in order to get the information from the error itself instead of making a guess?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add your description then it is finally lost internet connection, users should also be aware that they are lost connection. On the other hand we have the fallback of trying to retry some requests a number of times if they fail as in your case. For mobile we also support offline mode.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

users should also be aware that they are lost connection

100% agreed with that. And I imagine you have a watcher somewhere to check the connexion status.

What I find problematic here is the fact that we might end up returning the wrong reason for the error. Can’t we tell that it’s a connection issue directly from the initial error, without making this connectivity check?

And maybe, since this is the only error the user gets, we’ll display “We’re having issues with your internet connection, please try again later”, when in fact, it’s a real server error, so we’d be showing something incorrect to the user.

It’s actually because of this call that you have to change all the catch blocks to async, right?

log('AuthorizationInterceptors::onError: Perform get New Token');

final newToken = PlatformInfo.isIOS
? await _getNewTokenForIOSPlatform()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the flow for iOS:

We set a shouldRefresh boolean to true, and then we call this _refreshToken method. So we know we need to refresh the token because we had an issue with the previous token.

But then, for iOS, we look if we have something within the keychain, and if we've something within the keychain, and this something is not expired then, we return the same token. I don't know if the check we do for setting shouldRefresh to true, is the same than we do in _isTokenExpired but it can be source of bug.

Why not just force the refresh even for iOS in this case since we already know we need to refresh?

And with this forceRefresh, I'm pretty sure, we'll be able to remove this condition:

if (newToken.token == _token?.token) {
      log('AuthorizationInterceptors::onError: Token duplicated');
      return null;
    }

No?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • For iOS, we have to handle notification in native code, due to the limitations of flutter, so we need to handle refresh token in native code to avoid getting data and getting 401 error on native. This token is synchronized with dart code on flutter through keychain

  • We can't remove this.

if (newToken.token == _token?.token) {
log('AuthorizationInterceptors::onError: Token duplicated');
return null;
}

Because we added it to fix some issues with running multiple requests on multiple isolates at the same time

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation (I'll check where do we do native code)

In _getNewTokenForIOSPlatform why not then use:

_isTokenNotEmpty(tokenOIDC)
      && _isRefreshTokenNotEmpty(tokenOIDC)
      && _isTokenExpired(tokenOIDC);

instead of "just" isTokenExpired? Like that we're doing the same check than the previous one.

But I still don't understand, in that case (where we have multiple request at once from native & dart runtime) why having an old token equal to the new one is an error? To me its say "ok my token has been refreshed during my call, let's continue"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh my bad, it's just a log from "onError", sorry for the last point.

@dab246 dab246 requested a review from Crash-- November 12, 2025 11:18
@chibenwa
Copy link
Member

chibenwa commented Nov 13, 2025

Please be hereby formally reminded that this bug till impacts one of our major customer.
Please carry out his work as results are xpected from the team.
(This work is in standby for over a month now)

@hoangdat
Copy link
Member

No, not a standby, we built it and monitoring the app through a month. But the problem occurred on last Tuesday, so we are investigating

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

Successfully merging this pull request may close these issues.

5 participants