Skip to content

Commit

Permalink
feat: Rework error handling, upgrade dio, fixed bugs
Browse files Browse the repository at this point in the history
- Fix grey screen bug when adding labels from documnet upload
- Add more permission checks to conditionally show widgets
  • Loading branch information
astubenbord committed Jul 22, 2023
1 parent c4f2810 commit 6566b2b
Show file tree
Hide file tree
Showing 70 changed files with 1,444 additions and 1,131 deletions.
107 changes: 27 additions & 80 deletions lib/core/interceptor/dio_http_error_interceptor.dart
Original file line number Diff line number Diff line change
@@ -1,99 +1,46 @@
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/type/types.dart';

class DioHttpErrorInterceptor extends Interceptor {
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 400) {
// try to parse contained error message, otherwise return response
final dynamic data = err.response?.data;
if (data is Map<String, dynamic>) {
return _handlePaperlessValidationError(data, handler, err);
} else if (data is String) {
return _handlePlainError(data, handler, err);
}
} else if (err.response?.statusCode == 403) {
var data = err.response!.data;
if (data is Map && data.containsKey("detail")) {
final data = err.response!.data;
if (PaperlessServerMessageException.canParse(data)) {
final exception = PaperlessServerMessageException.fromJson(data);
final message = exception.detail;
handler.reject(
DioError(
message: data['detail'],
DioException(
message: message,
requestOptions: err.requestOptions,
error: ServerMessageException(data['detail']),
error: exception,
response: err.response,
type: DioErrorType.unknown,
type: DioExceptionType.badResponse,
),
);
return;
}
} else if (err.error is SocketException) {
final ex = err.error as SocketException;
if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) {
return handler.reject(
DioError(
message: "The server could not be reached. Is the device offline?",
error: const PaperlessServerException(ErrorCode.deviceOffline),
} else if (PaperlessFormValidationException.canParse(data)) {
final exception = PaperlessFormValidationException.fromJson(data);
handler.reject(
DioException(
requestOptions: err.requestOptions,
type: DioErrorType.connectionTimeout,
error: exception,
response: err.response,
type: DioExceptionType.badResponse,
),
);
}
}
return handler.reject(err);
}

void _handlePaperlessValidationError(
Map<String, dynamic> json,
ErrorInterceptorHandler handler,
DioError err,
) {
final PaperlessValidationErrors errorMessages = {};
for (final entry in json.entries) {
if (entry.value is List) {
errorMessages.putIfAbsent(
entry.key,
() => (entry.value as List).cast<String>().first,
} else if (data is String &&
data.contains("No required SSL certificate was sent")) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
type: DioExceptionType.badResponse,
error:
const PaperlessApiException(ErrorCode.missingClientCertificate),
),
);
} else if (entry.value is String) {
errorMessages.putIfAbsent(entry.key, () => entry.value);
} else {
errorMessages.putIfAbsent(entry.key, () => entry.value.toString());
}
} else {
return handler.next(err);
}
handler.reject(
DioError(
error: errorMessages,
requestOptions: err.requestOptions,
type: DioErrorType.badResponse,
),
);
}

void _handlePlainError(
String data,
ErrorInterceptorHandler handler,
DioError err,
) {
if (data.contains("No required SSL certificate was sent")) {
handler.reject(
DioError(
requestOptions: err.requestOptions,
type: DioErrorType.badResponse,
error: const PaperlessServerException(
ErrorCode.missingClientCertificate),
),
);
}
}
}

enum _OsErrorCodes {
serverUnreachable(101);

const _OsErrorCodes(this.code);
final int code;
}
32 changes: 32 additions & 0 deletions lib/core/interceptor/dio_offline_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'dart:io';

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

class DioOfflineInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.error is SocketException) {
final ex = err.error as SocketException;
if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) {
handler.reject(
DioException(
message: "The host could not be reached. Is your device offline?",
error: const PaperlessApiException(ErrorCode.deviceOffline),
requestOptions: err.requestOptions,
type: DioExceptionType.connectionTimeout,
),
);
}
} else {
handler.next(err);
}
}
}

enum _OsErrorCodes {
serverUnreachable(101);

const _OsErrorCodes(this.code);
final int code;
}
27 changes: 27 additions & 0 deletions lib/core/interceptor/dio_unauthorized_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';

class DioUnauthorizedInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 403) {
final data = err.response!.data;
String? message;
if (PaperlessServerMessageException.canParse(data)) {
final exception = PaperlessServerMessageException.fromJson(data);
message = exception.detail;
}
handler.reject(
DioException(
message: message,
requestOptions: err.requestOptions,
error: PaperlessUnauthorizedException(message),
response: err.response,
type: DioExceptionType.badResponse,
),
);
} else {
handler.next(err);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class RetryOnConnectionChangeInterceptor extends Interceptor {
});

@override
void onError(DioError err, ErrorInterceptorHandler handler) async {
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (_shouldRetryOnHttpException(err)) {
try {
handler.resolve(await DioHttpRequestRetrier(dio: dio)
Expand All @@ -27,8 +27,8 @@ class RetryOnConnectionChangeInterceptor extends Interceptor {
}
}

bool _shouldRetryOnHttpException(DioError err) {
return err.type == DioErrorType.unknown &&
bool _shouldRetryOnHttpException(DioException err) {
return err.type == DioExceptionType.unknown &&
(err.error is HttpException &&
(err.message?.contains(
'Connection closed before full header was received',
Expand Down
10 changes: 5 additions & 5 deletions lib/core/interceptor/server_reachability_error_interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class ServerReachabilityErrorInterceptor extends Interceptor {
static const _missingClientCertText = "No required SSL certificate was sent";

@override
void onError(DioError err, ErrorInterceptorHandler handler) {
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 400) {
final message = err.response?.data;
if (message is String && message.contains(_missingClientCertText)) {
Expand All @@ -19,7 +19,7 @@ class ServerReachabilityErrorInterceptor extends Interceptor {
);
}
}
if (err.type == DioErrorType.connectionTimeout) {
if (err.type == DioExceptionType.connectionTimeout) {
return _rejectWithStatus(
ReachabilityStatus.connectionTimeout,
err,
Expand Down Expand Up @@ -48,13 +48,13 @@ class ServerReachabilityErrorInterceptor extends Interceptor {

void _rejectWithStatus(
ReachabilityStatus reachabilityStatus,
DioError err,
DioException err,
ErrorInterceptorHandler handler,
) {
handler.reject(DioError(
handler.reject(DioException(
error: reachabilityStatus,
requestOptions: err.requestOptions,
response: err.response,
type: DioErrorType.unknown,
type: DioExceptionType.unknown,
));
}
2 changes: 2 additions & 0 deletions lib/core/navigation/push_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,15 @@ Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
final labelRepo = context.read<LabelRepository>();
final docsApi = context.read<PaperlessDocumentsApi>();
final connectivity = context.read<Connectivity>();
final apiVersion = context.read<ApiVersion>();
return Navigator.of(context).push<DocumentUploadResult>(
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: labelRepo),
Provider.value(value: docsApi),
Provider.value(value: connectivity),
Provider.value(value: apiVersion)
],
builder: (_, child) => BlocProvider(
create: (_) => DocumentUploadCubit(
Expand Down
1 change: 0 additions & 1 deletion lib/core/repository/label_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
if (correspondent != null) {
final updatedState = {...state.correspondents}..[id] = correspondent;
emit(state.copyWith(correspondents: updatedState));

return correspondent;
}
return null;
Expand Down
16 changes: 11 additions & 5 deletions lib/core/security/session_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';

/// Manages the security context, authentication and base request URL for
/// an underlying [Dio] client which is injected into all services
/// requiring authenticated access to the Paperless HTTP API.
/// requiring authenticated access to the Paperless REST API.
class SessionManager extends ValueNotifier<Dio> {
Dio get client => value;

Expand All @@ -20,16 +21,21 @@ class SessionManager extends ValueNotifier<Dio> {
static Dio _initDio(List<Interceptor> interceptors) {
//en- and decoded by utf8 by default
final Dio dio = Dio(
BaseOptions(contentType: Headers.jsonContentType),
BaseOptions(
contentType: Headers.jsonContentType,
followRedirects: true,
maxRedirects: 10,
),
);
dio.options
..receiveTimeout = const Duration(seconds: 30)
..sendTimeout = const Duration(seconds: 60)
..responseType = ResponseType.json;
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
(client) => client..badCertificateCallback = (cert, host, port) => true;
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient =
() => HttpClient()..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll([
...interceptors,
DioUnauthorizedInterceptor(),
DioHttpErrorInterceptor(),
PrettyDioLogger(
compact: true,
Expand Down Expand Up @@ -64,7 +70,7 @@ class SessionManager extends ValueNotifier<Dio> {
password: clientCertificate.passphrase,
);
final adapter = IOHttpClientAdapter()
..onHttpClientCreate = (client) => HttpClient(context: context)
..createHttpClient = () => HttpClient(context: context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;

Expand Down
4 changes: 2 additions & 2 deletions lib/core/service/connectivity_status_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
return ReachabilityStatus.reachable;
}
return ReachabilityStatus.notReachable;
} on DioError catch (error) {
if (error.type == DioErrorType.unknown &&
} on DioException catch (error) {
if (error.type == DioExceptionType.unknown &&
error.error is ReachabilityStatus) {
return error.error as ReachabilityStatus;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/core/service/file_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class FileService {
) async {
final dir = await documentsDirectory;
if (dir == null) {
throw const PaperlessServerException.unknown(); //TODO: better handling
throw const PaperlessApiException.unknown(); //TODO: better handling
}
File file = File("${dir.path}/$filename");
return file..writeAsBytes(bytes);
Expand Down
Loading

0 comments on commit 6566b2b

Please sign in to comment.