Skip to content

Instantly share code, notes, and snippets.

@iliyaZelenko
Created February 7, 2024 20:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iliyaZelenko/781a9065da875b563be5d2f79ff61a33 to your computer and use it in GitHub Desktop.
Save iliyaZelenko/781a9065da875b563be5d2f79ff61a33 to your computer and use it in GitHub Desktop.
DioRefreshTokenInterceptor
import 'dart:io';
import 'package:dio/dio.dart';
import '../app_http_client.dart';
import '../app_http_client_token_refresher.dart';
import '../app_http_exception.dart';
import '../http_method.dart';
// TODO Ilya: lock all requests while refreshing token
class DioRefreshTokenInterceptor extends QueuedInterceptor {
final AppHttpClient _httpClient;
final AppHttpClientTokenRefresher _refresher;
DioRefreshTokenInterceptor({
required AppHttpClient httpClient,
required AppHttpClientTokenRefresher refresher,
}) : _httpClient = httpClient,
_refresher = refresher;
@override
Future<void> onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
print(
'HTTP INTERCEPTOR ${err.requestOptions.uri} (${err.requestOptions.method}) $err');
if (err.response == null) {
return handler.next(err);
} else if (err.response!.statusCode == 401) {
if (_httpClient.refreshToken == null) {
throw AppHttp401Exception(AppHttpException(
requestOptions: err.requestOptions,
error: err.error,
response: err.response,
));
}
try {
await _refresher.refresh(_httpClient);
} catch (e) {
if (e is AppHttp401Exception) {
rethrow;
} else if (e is DioException) {
return handler.next(err);
}
rethrow;
}
try {
// If refreshed, then retry
return handler.resolve(await _retry(err.requestOptions));
} catch (e) {
if (e is DioException) {
// Если будет плохая свзять, то следующий интерцептор обработает для ретрайа
return handler.next(err);
}
rethrow;
}
}
handler.next(err);
}
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
// Use new token
requestOptions.headers[HttpHeaders.authorizationHeader] =
'Bearer ${_httpClient.token}';
final withProtocol = requestOptions.uri.host.startsWith('http');
final host = '${withProtocol ? '' : 'https://'}${requestOptions.uri.host}';
return _httpClient.performRequest(
host: host,
path: requestOptions.uri.path,
query: requestOptions.queryParameters,
body: requestOptions.data,
headers: requestOptions.headers,
method: HttpMethod.values.byName(requestOptions.method.toLowerCase()),
);
}
}
@iliyaZelenko
Copy link
Author

import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:app_http_client/app_http_client.dart';
import 'package:dio/dio.dart';

import '../app_http_client.dart';
import '../app_http_client_token_refresher.dart';
import '../app_http_client_token_refresher_impl.dart';
import '../app_http_exception.dart';
import '../http_method.dart';
import '../tokens_storage.dart';
import 'dio_exception_adapter.dart';
import 'dio_refresh_token_interceptor.dart';
import 'dio_retry_interceptor.dart';

class DioHttpClientImpl implements AppHttpClient {
  final TokensStorage _tokensStorage;
  final BaseUrlType _defaultHost;
  final Dio _dio;
  final DioExceptionAdapter _exceptionAdapter;
  final AppHttpClientTokenRefresher _tokenRefresher;
  final Transformer? _transformer;
  final Iterable<Interceptor> _interceptors;

  DioHttpClientImpl({
    required BaseUrlType defaultHost,
    Dio? dio,
    TokensStorage? tokensStorage,
    DioExceptionAdapter? exceptionAdapter,
    AppHttpClientTokenRefresher? tokenRefresher,
    OnTokenRefreshedType? onTokenRefreshed,
    Transformer? transformer,
    Iterable<Interceptor> interceptors = const Iterable.empty(),
  })  : _defaultHost = defaultHost,
        _dio = dio ?? Dio(BaseOptions(
            // validateStatus: (int? status) {
            //   // Fix https://github.com/flutterchina/dio/issues/995#issuecomment-739902537
            //   return status != null && status >= 100 && status <= 400;
            // },
            )),
        _transformer = transformer,
        _interceptors = interceptors,
        _tokensStorage = tokensStorage ?? TokensStorageImpl(),
        _exceptionAdapter = exceptionAdapter ?? const DioExceptionAdapter(),
        _tokenRefresher =
            tokenRefresher ?? AppHttpClientTokenRefresherImpl(onTokenRefreshed);

  @override
  String get defaultHost => _defaultHost();

  @override
  String? get token => _tokensStorage.token;

  @override
  String? get refreshToken => _tokensStorage.refreshToken;

  @override
  Future<void> init() async {
    // See https://stackoverflow.com/a/62911616/5286034
    final transformer = _transformer;
    if (transformer != null) _dio.transformer = transformer;
    _dio
      ..interceptors.addAll([
        ..._interceptors,
        DioRefreshTokenInterceptor(
          httpClient: this,
          refresher: _tokenRefresher,
        ),
        DioRetryInterceptor(
          dio: _dio,
          retryableStatuses: {502, 503, 429},
          // retryEvaluator: DioRetryEvaluator({502, 503, 429}).evaluate,
          retryDelays: () => [
            // Will be used for all retries
            Duration(seconds: Random().nextInt(30 + 1) + 10)
          ],
          // Грубо говоря 1440 запрсов за 12 часов если максимальная delay из retryDelays - 30 секунд
          retries: (60 * 60 * 12 / 30).floor(),
        ),
      ]);
    // ..httpClientAdapter = Http2Adapter(
    //   ConnectionManager(
    //     idleTimeout: 10000,
    //     // Ignore bad certificate
    //     onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
    //   ),
    // );
    await _tokensStorage.init();
  }

  @override
  Future<void> refresh() => _tokenRefresher.refresh(this);

  // @throws AppHttpException if http error
  @override
  Future<Response<T>> performRequest<T>({
    String? host,
    String path = '',
    Map<String, dynamic>? query,
    Map<String, dynamic>? headers,
    Map<String, dynamic>? body,
    required HttpMethod method,
    Map<String, String>? fields,
  }) async {
    final requestHost = host ?? defaultHost;
    final haveContentTypeHeader = headers?.keys.any((key) =>
            key.toLowerCase() == HttpHeaders.contentTypeHeader.toLowerCase()) ??
        false;
    final internalHeaders = <String, dynamic>{
      if (!haveContentTypeHeader)
        HttpHeaders.contentTypeHeader:
            ContentType('application', 'json', charset: 'utf-8').toString(),
      if (token != null && token!.isNotEmpty)
        // На имени HttpHeaders.authorizationHeader завязан другой код!
        HttpHeaders.authorizationHeader: 'Bearer $token',
      HttpHeaders.acceptHeader:
          ContentType('application', 'json', charset: 'utf-8').toString(),
      ...(headers ?? {}),
    };

    late Response<T> response;

    try {
      response = await _dio.request<T>(
        requestHost + path,
        data: body,
        queryParameters: query,
        options: Options(
          method: method.toString().split('.').last,
          headers: internalHeaders,
        ),
      );
    } catch (e) {
      print('ERR!');
      print(e);
      if (e is DioException && e.response != null) {
        var exception = AppHttpException(
          requestOptions: e.requestOptions,
          error: e.error,
          response: e.response,
          type: _exceptionAdapter.adapt(e.type),
        );

        if (e.response!.statusCode == 401) {
          exception = AppHttp401Exception(exception);
        }

        throw exception;
      }

      rethrow;
    }

    await _rememberTokensFromResponse<T>(response);

    return response;
  }

  @override
  Future<Response<T>> get<T>({
    String? host,
    String path = '',
    Map<String, dynamic>? query,
    Map<String, String>? headers,
  }) {
    return performRequest<T>(
      method: HttpMethod.get,
      host: host,
      path: path,
      query: query,
      headers: headers,
    );
  }

  @override
  Future<Response<T>> post<T>({
    String? host,
    String path = '',
    Map<String, dynamic>? query,
    Map<String, String>? headers,
    Map<String, dynamic>? body,
  }) {
    return performRequest<T>(
      method: HttpMethod.post,
      host: host,
      path: path,
      query: query,
      headers: headers,
      body: body,
    );
  }

  @override
  Future<Response<T>> put<T>({
    String? host,
    String path = '',
    Map<String, dynamic>? query,
    Map<String, String>? headers,
    Map<String, dynamic>? body,
  }) {
    return performRequest<T>(
      method: HttpMethod.put,
      host: host,
      path: path,
      query: query,
      headers: headers,
      body: body,
    );
  }

  @override
  Future<Response<T>> delete<T>({
    String? host,
    String path = '',
    Map<String, dynamic>? query,
    Map<String, String>? headers,
    Map<String, dynamic>? body,
  }) {
    return performRequest<T>(
      method: HttpMethod.delete,
      host: host,
      path: path,
      query: query,
      headers: headers,
      body: body,
    );
  }

  @override
  Future<Response<T>> patch<T>({
    String? host,
    String path = '',
    Map<String, dynamic>? query,
    Map<String, String>? headers,
    Map<String, dynamic>? body,
  }) {
    return performRequest<T>(
      method: HttpMethod.patch,
      host: host,
      path: path,
      query: query,
      headers: headers,
      body: body,
    );
  }

  @override
  Future<Response<T>> filesPost<T>({
    String? host,
    String path = '',
    Map<String, String>? headers,
    Map<String, String>? query,
    required files,
    Map<String, String>? fields,
  }) {
    return performRequest<T>(
      method: HttpMethod.filePost,
      host: host,
      path: path,
      headers: headers,
      query: query,
      fields: fields,
    );
  }

  @override
  Future<void> clearTokens() => _tokensStorage.clear();

  @override
  Future<void> rememberTokens(
    String? token,
    String? refreshToken,
  ) =>
      _tokensStorage.save(
        token,
        refreshToken,
      );

  Future<void> _rememberTokensFromResponse<T>(Response<T> value) async {
    try {
      final data = value.data;

      // TODO Ilya: check
      if (data is Map?) {
        await rememberTokens(
          data?[TokensStorageKeys.token] as String?,
          data?[TokensStorageKeys.refreshToken] as String?,
        );
      }
    } catch (_) {}
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment