import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:http/http.dart' as http; import 'package:nextcloud/nextcloud.dart'; import '../../session/session.dart'; import '../api_error.dart'; import '../http_errors.dart'; import '../marianumcloud/talk/talk_error.dart'; import 'app_exception.dart'; import 'network_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; } } final RegExp _davException = RegExp( r'\s*([^<]*?)\s*', ); final RegExp _davMessage = RegExp(r'\s*([^<]*?)\s*'); /// The Sabre DAV error body of a failed WebDAV request: the exception class /// (without its `Sabre\DAV\Exception\` namespace) and the server's message. /// The class says what actually went wrong where the status is ambiguous — /// MKCOL on an existing folder is a plain 405. ({String exception, String message})? parseDavError(String body) { final exception = _davException.firstMatch(body)?.group(1); if (exception == null || exception.isEmpty) return null; return ( exception: exception.split('\\').last, message: _davMessage.firstMatch(body)?.group(1) ?? '', ); } AppException? _davToAppException( int status, ({String exception, String message}) dav, String detail, ) { final userMessage = switch (dav.exception) { 'MethodNotAllowed' when status == 405 => 'Ein Ordner oder eine Datei mit diesem Namen existiert bereits.', 'PreconditionFailed' => 'Am Zielort existiert bereits ein Eintrag mit diesem Namen.', 'Conflict' => 'Der übergeordnete Ordner existiert nicht mehr.', 'Locked' => 'Der Eintrag wird gerade bearbeitet. Bitte versuche es gleich erneut.', 'InsufficientStorage' => 'Der verfügbare Speicherplatz reicht dafür nicht aus.', _ => null, }; if (userMessage == null) return null; return ServerException( statusCode: status, userMessage: userMessage, technicalDetails: detail, allowRetry: dav.exception == 'Locked', ); } /// 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 dav = parseDavError(error.body); if (dav != null) { final detail = 'HTTP $status ${dav.exception}: ${dav.message}'; return _davToAppException(status, dav, detail) ?? exceptionForStatus(status, detail); } final preview = previewBody(error.body); final detail = preview.isEmpty ? 'HTTP $status' : 'HTTP $status body=$preview'; if (status == 429) { return ServerException( statusCode: status, userMessage: 'Zu viele Anfragen. Bitte warte einen Moment, bevor du es erneut versuchst.', technicalDetails: detail, ); } return exceptionForStatus(status, 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 NextcloudUnavailableException) { return 'Diese Funktion ist mit deinem Konto nicht verfügbar.'; } 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 NextcloudUnavailableException) return false; 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; }