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

DioException on 401 before hitting an Authentication Interceptor #2056

Closed
JonasLykkeIOspect opened this issue Dec 4, 2023 · 16 comments
Closed
Labels
i: duplicate This issue or pull request already exists

Comments

@JonasLykkeIOspect
Copy link

JonasLykkeIOspect commented Dec 4, 2023

Package

dio

Version

5.4.0

Operating-System

Android

Output of flutter doctor -v

[√] Flutter (Channel stable, 3.16.2, on Microsoft Windows [Version 10.0.22000.2538], locale da-DK)
    • Flutter version 3.16.2 on channel stable at C:\Users\jl\AppData\Local\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 9e1c857886 (4 days ago), 2023-11-30 11:51:18 -0600
    • Engine revision cf7a9d0800
    • Dart version 3.2.2
    • DevTools version 2.28.3

[√] Windows Version (Installed version of Windows is version 10 or higher)

[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at C:\Users\jl\AppData\Local\Android\Sdk
    • Platform android-34, build-tools 34.0.0
    • ANDROID_HOME = C:\Users\jl\AppData\Local\Android\Sdk
    • Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-b2043.56-9586694)
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[!] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.8.2)
    • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
    • Visual Studio Community 2022 version 17.8.34322.80
    X Visual Studio is missing necessary components. Please re-run the Visual Studio installer for the "Desktop development with C++" workload, and include these components:
        MSVC v142 - VS 2019 C++ x64/x86 build tools
         - If there are multiple build tool versions available, install the latest
        C++ CMake tools for Windows
        Windows 10 SDK

[√] Android Studio (version 2022.2)
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-b2043.56-9586694)

[√] VS Code (version 1.84.2)
    • VS Code at C:\Users\jl\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.78.0

[√] Connected device (4 available)
    • sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64    • Android 14 (API 34) (emulator)
    • Windows (desktop)            • windows       • windows-x64    • Microsoft Windows [Version 10.0.22000.2538]
    • Chrome (web)                 • chrome        • web-javascript • Google Chrome 119.0.6045.160
    • Edge (web)                   • edge          • web-javascript • Microsoft Edge 119.0.2151.93

[√] Network resources
    • All expected network resources are available.

! Doctor found issues in 1 category.

Dart Version

3.2.2

Steps to Reproduce

I have this Intercepter:

class AuthenticationInterceptor extends Interceptor {
  final Dio _dio;
  final String _apiBaseUrl;

  AuthenticationInterceptor(
    this._dio,
    this._apiBaseUrl,
  );

  @override
  Future<void> onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    // Retrieve the authentication token from local storage
    final authenticationToken = await LocalAuthenticationTokenIsarService.get();

    // Extract the access token from the authentication token
    final accessToken = authenticationToken?.accessToken;

    // Add the access token to the request header
    options.headers['Authorization'] = 'Bearer $accessToken';

    return handler.next(options);
  }

  @override
  Future<void> onError(
      DioException err, ErrorInterceptorHandler handler) async {
    // Check if the response status code is 401 (Unauthorized) and it's not the login request
    if (err.response?.statusCode == 401 &&
        err.response?.requestOptions.path != authenticationLoginUrl) {
      // Retrieve the authentication token from local storage
      final authenticationToken =
          await LocalAuthenticationTokenIsarService.get();

      // Extract the refresh token from the authentication token
      final refreshToken = authenticationToken?.refreshToken;

      // If a 401 response is received, refresh the access token
      String? newAccessToken =
          await _getAuthenticationToken(_dio, _apiBaseUrl, refreshToken ?? '');

      if (newAccessToken != null) {
        // Update the request header with the new access token
        err.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';

        // Repeat the request with the updated header
        return handler.resolve(await _dio.fetch(err.requestOptions));
      }
    }

    return handler.next(err);
  }

  Future<String?> _getAuthenticationToken(
      Dio dio, String apiBaseUrl, String refreshToken) async {
    try {
      // Send a request to refresh the access token using the refresh token
      final response = await dio.post(
        '$apiBaseUrl$authenticationRefreshUrl',
        data: {'refreshToken': refreshToken},
      );

      // Check if the response status code is 200 (OK)
      if (response.statusCode == 200) {
        // Parse the response to get the new authentication token
        final AuthenticationToken authenticationToken =
            AuthenticationToken.fromJson(response.data);

        // Update the local storage with the new authentication token
        await LocalAuthenticationTokenIsarService.addUpdate(
            authenticationToken);

        // Return the new access token
        return authenticationToken.accessToken;
      } else {
        await LocalAuthenticationTokenIsarService.delete();
      }

      // Return null if the response status code is not 200
      return null;
    } catch (error) {
      // Handle any exceptions or errors that occurred during the process
      logger.e('Error refreshing authentication token: $error');
      // You may choose to rethrow the error, return a default value, or handle it in another way
      return null;
    }
  }
}

const String _apiBaseUrl = String.fromEnvironment('API_BASE_URL');

@riverpod
Dio dio(DioRef ref) {
  final dio = Dio();

  dio.interceptors.add(AuthenticationInterceptor(dio, _apiBaseUrl));

  return dio;
}

Expected Result

When I call an API that returns a 401, because the token has expired, the onError method should get a new token from the API.
And then its all good

Actual Result

This have worked, is have now stopped working,
Is gets to the "onRequest" in the Interfecpter, and sets the header, but it never gets to the "onError"
and I get a DioException instead:

Its thrown from dio_mixin.dart in the method "_dispatchRequest" -> (Line 564) throw assureDioException(e, reqOpt);

DioException

Exception has occurred.
DioException (DioException [bad response]: This exception was thrown because the response has a status code of 401 and RequestOptions.validateStatus was configured to throw for this status code.
The status code of 401 has the following meaning: "Client error - the request contains bad syntax or cannot be fulfilled"
Read more about status codes at https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
In order to resolve this exception you typically have either to verify and fix your request code or you have to fix the server code.
)

@JonasLykkeIOspect JonasLykkeIOspect added h: need triage This issue needs to be categorized s: bug Something isn't working labels Dec 4, 2023
@AlexV525
Copy link
Member

AlexV525 commented Dec 4, 2023

  1. Could you try with QueuedInterceptor?
  2. Looks like some request might sneak out from your interceptor based on the condition err.response?.requestOptions.path != authenticationLoginUrl.

@JonasLykkeIOspect
Copy link
Author

@AlexV525
I've tried QueuedInterceptor and also comment the part with the url out

@override
  Future<void> onError(
      DioException err, ErrorInterceptorHandler handler) async {
    // Check if the response status code is 401 (Unauthorized) and it's not the login request
    if (err.response?.statusCode == 401
        // && err.response?.requestOptions.path != authenticationLoginUrl
        ) {
      // Retrieve the authentication token from local storage
      final authenticationToken =
          await LocalAuthenticationTokenIsarService.get();

      // Extract the refresh token from the authentication token
      final refreshToken = authenticationToken?.refreshToken;

      // If a 401 response is received, refresh the access token
      String? newAccessToken =
          await _getAuthenticationToken(_dio, _apiBaseUrl, refreshToken ?? '');

      if (newAccessToken != null) {
        // Update the request header with the new access token
        err.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';

        // Repeat the request with the updated header
        return handler.resolve(await _dio.fetch(err.requestOptions));
      }
    }

    return handler.next(err);
  }

I still the request goes fine through the onRequest and gets shipped out with return handler.next(options)
But before getting to either onResponse or onError. I get that DioException again from the dio_mixin.dart. If I continue from that throw, the rest goes as it should. I get to the onError, and I get a now Token.

How and why i'm getting an DioException before, I realy can't figure out

@Zhamshid
Copy link

Zhamshid commented Dec 5, 2023

The same error, version 5.4.0

@AlexV525
Copy link
Member

AlexV525 commented Dec 5, 2023

I'm currently AFK. Is it working in a previous version and break in the latest version?

@JonasLykkeIOspect
Copy link
Author

JonasLykkeIOspect commented Dec 5, 2023

@AlexV525
I just tested a few versions of Dio, unfortunately get the same problem.

But when I downgrade Flutter to version 3.13.9
Then I get to the "onError" method before the DioException.
If I then upgrade the Flutter version to a newer one, the DioException comes first

Flutter 3.13.9:
DioException < onError

Flutter 3.16.x:
DioException > onError

@AlexV525
Copy link
Member

AlexV525 commented Dec 5, 2023

Oh I overlook the issue. This is not related to any actual code. Duplicate of #1869

@AlexV525 AlexV525 closed this as not planned Won't fix, can't repro, duplicate, stale Dec 5, 2023
@AlexV525 AlexV525 added i: duplicate This issue or pull request already exists and removed h: need triage This issue needs to be categorized s: bug Something isn't working labels Dec 5, 2023
@samg-hub
Copy link

the same error
image

ive got this error when i upgrade flutter to version 3.16.1

@hupo376787

This comment was marked as off-topic.

@JonasLykkeIOspect
Copy link
Author

JonasLykkeIOspect commented Dec 17, 2023

If I run my app WITH debug (VS Code F5), I get this error on Flutter 3.16.x
When i run my app WITHOUT debug (VS Code Ctrl+F5). I don't get the error.

@shakthizen
Copy link

I found a hacky solution. We are overriding all the exceptions thrown by default. Then we are going to create custom interceptor to handle and throw exceptions. This way we can catch the DioException as we did before.

  1. Create a base client like this. You have to set validateStatus function to return true whatever the status code is.
final baseOptions = BaseOptions(
  baseUrl: host,
  contentType: Headers.jsonContentType,
  validateStatus: (int? status) {
    return status != null;
    // return status != null && status >= 200 && status < 300;
  },
);

final dio = Dio(baseOptions);
  1. Create a custom interceptor to raise exceptions
class ErrorInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final status = response.statusCode;
    final isValid = status != null && status >= 200 && status < 300;
    if (!isValid) {
      throw DioException.badResponse(
        statusCode: status!,
        requestOptions: response.requestOptions,
        response: response,
      );
    }
    super.onResponse(response, handler);
  }
}
  1. Add that to the Dio interceptors list
dio.interceptors.addAll([
    ErrorInterceptor(),
]);

Link to my gist https://gist.github.com/shakthizen/d644aaf19ca837a4a29a92ebad551055

@jackie-weiwei
Copy link

When you use vscode to debug, just cancel the Uncaught Exception in 'BREAKPOINTS',as the picture shows:
image

@d1ss0nanz
Copy link

I keep running into this issue for some time now (has worked for 1+ years before) with flutter web.

My app is not running with the debugger. It's build with the following command:
flutter build web --no-tree-shake-icons --web-renderer canvaskit

The app is using an QueuedInterceptorsWrapper, but the onError callback is never called.

Instead these messages started showing up:
Screenshot 2024-02-06 at 12 47 04

So I guess, the issue is not (only) caused by the debugger?

@jackie-weiwei
Copy link

@d1ss0nanz
It is normal to report 401. You can just handle this exception:
401 Unauthorized
Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
If it is a double token, you can handle this exception and refresh the token in this processed method.

@d1ss0nanz
Copy link

d1ss0nanz commented Feb 7, 2024

The onError handler of the Dio interceptor is not getting called. That’s were the reauthentication is handled (using a refresh token).

It worked for a long time and broke recently.

@nurullahturkoglu
Copy link

nurullahturkoglu commented Mar 26, 2024

Any update?

update: I did find this solution which it work for me: https://stackoverflow.com/questions/69427612/dioerror-dioerrortype-response-http-status-error-401

@don-pironet-hatch
Copy link

Seems fixed in the latest version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
i: duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

10 participants