154 lines
5.1 KiB
Dart
154 lines
5.1 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:nextcloud/nextcloud.dart';
|
|
|
|
import '../api_error.dart';
|
|
import '../http_errors.dart';
|
|
import '../marianumcloud/talk/talk_error.dart';
|
|
import 'app_exception.dart';
|
|
import 'auth_exception.dart';
|
|
import 'network_exception.dart';
|
|
import 'not_found_exception.dart';
|
|
import 'parse_exception.dart';
|
|
import 'server_exception.dart';
|
|
import 'talk_exception.dart';
|
|
|
|
const String _defaultFallback =
|
|
'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
|
|
const String _tlsErrorMessage =
|
|
'Die sichere Verbindung zum Server wurde abgelehnt (Zertifikat oder TLS-Fehler). '
|
|
'Häufige Ursachen: falsche Geräte-Uhrzeit oder ein WLAN mit Anmeldeseite (z.B. Café/Hotel).';
|
|
|
|
AppException? _dioToAppException(DioException error) {
|
|
switch (error.type) {
|
|
case DioExceptionType.connectionTimeout:
|
|
case DioExceptionType.sendTimeout:
|
|
case DioExceptionType.receiveTimeout:
|
|
case DioExceptionType.transformTimeout:
|
|
return NetworkException.timeout(technicalDetails: error.message);
|
|
case DioExceptionType.connectionError:
|
|
return NetworkException(technicalDetails: error.message);
|
|
case DioExceptionType.badCertificate:
|
|
return const NetworkException(userMessage: _tlsErrorMessage);
|
|
case DioExceptionType.badResponse:
|
|
final status = error.response?.statusCode;
|
|
return ServerException(
|
|
statusCode: status ?? -1,
|
|
technicalDetails: 'HTTP $status: ${error.message}',
|
|
);
|
|
case DioExceptionType.cancel:
|
|
case DioExceptionType.unknown:
|
|
final inner = error.error;
|
|
if (inner is SocketException) {
|
|
return NetworkException(technicalDetails: inner.message);
|
|
}
|
|
if (inner is HandshakeException) {
|
|
return const NetworkException(userMessage: _tlsErrorMessage);
|
|
}
|
|
if (inner is FormatException) {
|
|
return ParseException(technicalDetails: inner.message);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// The nextcloud/dynamite clients throw [DynamiteApiException] on non-2xx.
|
|
/// Its toString() dumps every response header, so the details keep only the
|
|
/// status plus a trimmed body preview (same format as the Talk API errors).
|
|
AppException _dynamiteToAppException(DynamiteApiException error) {
|
|
final status = error.statusCode;
|
|
final preview = previewBody(error.body);
|
|
final detail = preview.isEmpty ? 'HTTP $status' : 'HTTP $status body=$preview';
|
|
switch (status) {
|
|
case 401:
|
|
return AuthException.unauthorized(technicalDetails: detail);
|
|
case 403:
|
|
return AuthException.forbidden(technicalDetails: detail);
|
|
case 404:
|
|
return NotFoundException(technicalDetails: detail);
|
|
case 429:
|
|
return ServerException(
|
|
statusCode: status,
|
|
userMessage:
|
|
'Zu viele Anfragen. Bitte warte einen Moment, bevor du es erneut versuchst.',
|
|
technicalDetails: detail,
|
|
);
|
|
default:
|
|
return ServerException(statusCode: status, technicalDetails: detail);
|
|
}
|
|
}
|
|
|
|
String errorToUserMessage(Object? error, {String fallback = _defaultFallback}) {
|
|
if (error == null) return fallback;
|
|
if (error is AppException) return error.userMessage;
|
|
|
|
if (error is TalkError) return TalkException(error).userMessage;
|
|
|
|
if (error is DioException) {
|
|
final mapped = _dioToAppException(error);
|
|
if (mapped != null) return mapped.userMessage;
|
|
}
|
|
|
|
if (error is DynamiteApiException) {
|
|
return _dynamiteToAppException(error).userMessage;
|
|
}
|
|
|
|
if (error is SocketException) {
|
|
return const NetworkException().userMessage;
|
|
}
|
|
if (error is TimeoutException) {
|
|
return NetworkException.timeout().userMessage;
|
|
}
|
|
if (error is http.ClientException) {
|
|
return const NetworkException().userMessage;
|
|
}
|
|
if (error is HandshakeException) {
|
|
return _tlsErrorMessage;
|
|
}
|
|
if (error is FormatException) {
|
|
return const ParseException().userMessage;
|
|
}
|
|
if (error is ApiError) {
|
|
return _stripDioPrefix(error.message);
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
String? errorToTechnicalDetails(Object? error) {
|
|
if (error == null) return null;
|
|
if (error is AppException) return error.technicalDetails ?? error.toString();
|
|
if (error is TalkError) return TalkException(error).technicalDetails;
|
|
if (error is DioException) {
|
|
final mapped = _dioToAppException(error);
|
|
if (mapped != null) return mapped.technicalDetails ?? mapped.toString();
|
|
}
|
|
if (error is DynamiteApiException) {
|
|
final mapped = _dynamiteToAppException(error);
|
|
return mapped.technicalDetails ?? mapped.toString();
|
|
}
|
|
return error.toString();
|
|
}
|
|
|
|
bool errorAllowsRetry(Object? error) {
|
|
if (error == null) return true;
|
|
if (error is AppException) return error.allowRetry;
|
|
if (error is DioException) {
|
|
final mapped = _dioToAppException(error);
|
|
if (mapped != null) return mapped.allowRetry;
|
|
}
|
|
if (error is DynamiteApiException) {
|
|
return _dynamiteToAppException(error).allowRetry;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
String _stripDioPrefix(String raw) {
|
|
// ApiError messages embed full request URIs; only surface the first line.
|
|
final firstLine = raw.split('\n').first.trim();
|
|
return firstLine.isEmpty ? _defaultFallback : firstLine;
|
|
}
|