refactored HTTP error handling and streamlined UI components by centralizing shared logic and removing redundant parameters
This commit is contained in:
@@ -1 +0,0 @@
|
||||
class ApiRequest {}
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -59,9 +60,8 @@ AppException? _dioToAppException(DioException error) {
|
||||
/// status plus a trimmed body preview (same format as the Talk API errors).
|
||||
AppException _dynamiteToAppException(DynamiteApiException error) {
|
||||
final status = error.statusCode;
|
||||
final body = error.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
||||
final detail = body.isEmpty ? 'HTTP $status' : 'HTTP $status body=$preview';
|
||||
final preview = previewBody(error.body);
|
||||
final detail = preview.isEmpty ? 'HTTP $status' : 'HTTP $status body=$preview';
|
||||
switch (status) {
|
||||
case 401:
|
||||
return AuthException.unauthorized(technicalDetails: detail);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'errors/auth_exception.dart';
|
||||
import 'errors/network_exception.dart';
|
||||
import 'errors/not_found_exception.dart';
|
||||
import 'errors/server_exception.dart';
|
||||
|
||||
/// Runs [send] and converts transport-level failures (socket/timeout/client
|
||||
/// errors) into a [NetworkException] tagged with [label] (e.g. `Talk <uri>`).
|
||||
/// Passes through whatever [send] produces, including `null` for the base-class
|
||||
/// request hooks that may skip the call.
|
||||
Future<http.Response?> sendGuarded(
|
||||
String label,
|
||||
Future<http.Response>? Function() send,
|
||||
) async {
|
||||
try {
|
||||
return await send();
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(technicalDetails: '$label: ${e.message}');
|
||||
} on TimeoutException catch (e) {
|
||||
throw NetworkException.timeout(technicalDetails: '$label: $e');
|
||||
} on http.ClientException catch (e) {
|
||||
throw NetworkException(technicalDetails: '$label: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Collapses whitespace and caps an HTTP error body at 500 chars so it can be
|
||||
/// embedded in an [AppException]'s technical details without dumping headers.
|
||||
String previewBody(String body) {
|
||||
final collapsed = body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
return collapsed.length > 500 ? '${collapsed.substring(0, 500)}…' : collapsed;
|
||||
}
|
||||
|
||||
/// Builds a `<label> -> HTTP <status>[ body=<preview>]` technical detail line.
|
||||
String httpErrorDetail(String label, String body, int status) {
|
||||
final preview = previewBody(body);
|
||||
return preview.isEmpty
|
||||
? '$label -> HTTP $status'
|
||||
: '$label -> HTTP $status body=$preview';
|
||||
}
|
||||
|
||||
/// Throws the [AppException] matching a non-2xx HTTP [status], carrying
|
||||
/// [detail] as technical details: 401/403 map to auth errors, 404 to
|
||||
/// not-found, everything else to a generic server error.
|
||||
Never throwForStatus(int status, String detail) {
|
||||
if (status == 401) throw AuthException.unauthorized(technicalDetails: detail);
|
||||
if (status == 403) throw AuthException.forbidden(technicalDetails: detail);
|
||||
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
||||
throw ServerException(statusCode: status, technicalDetails: detail);
|
||||
}
|
||||
@@ -1,18 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../errors/auth_exception.dart';
|
||||
import '../../errors/network_exception.dart';
|
||||
import '../../errors/not_found_exception.dart';
|
||||
import '../../errors/parse_exception.dart';
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../../http_errors.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
|
||||
/// Mix of two Nextcloud surfaces:
|
||||
@@ -42,30 +37,17 @@ Future<http.Response> _send(
|
||||
) async {
|
||||
final headers = NextcloudOcs.headers();
|
||||
|
||||
final http.Response response;
|
||||
try {
|
||||
response = await perform(uri, headers);
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
||||
} on TimeoutException catch (e) {
|
||||
throw NetworkException.timeout(technicalDetails: 'Cloud $uri: $e');
|
||||
} on http.ClientException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
||||
}
|
||||
final response = (await sendGuarded(
|
||||
'Cloud $uri',
|
||||
() => perform(uri, headers),
|
||||
))!;
|
||||
|
||||
final status = response.statusCode;
|
||||
if (status >= 200 && status < 300) return response;
|
||||
|
||||
final body = response.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
||||
final detail = body.isEmpty
|
||||
? 'Cloud $uri -> HTTP $status'
|
||||
: 'Cloud $uri -> HTTP $status body=$preview';
|
||||
final detail = httpErrorDetail('Cloud $uri', response.body, status);
|
||||
log(detail);
|
||||
if (status == 401) throw AuthException.unauthorized(technicalDetails: detail);
|
||||
if (status == 403) throw AuthException.forbidden(technicalDetails: detail);
|
||||
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
||||
throw ServerException(statusCode: status, technicalDetails: detail);
|
||||
throwForStatus(status, detail);
|
||||
}
|
||||
|
||||
class SetUserAvatar {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../errors/network_exception.dart';
|
||||
import '../../../errors/server_exception.dart';
|
||||
import '../../../http_errors.dart';
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import 'get_chat_params.dart';
|
||||
import 'get_chat_response.dart';
|
||||
@@ -40,18 +37,12 @@ class LongPollChat {
|
||||
);
|
||||
final headers = NextcloudOcs.headers();
|
||||
|
||||
final http.Response response;
|
||||
try {
|
||||
response = await http
|
||||
final response = (await sendGuarded(
|
||||
'LongPollChat $uri',
|
||||
() => http
|
||||
.get(uri, headers: headers)
|
||||
.timeout(Duration(seconds: timeoutSeconds + 15));
|
||||
} on TimeoutException catch (e) {
|
||||
throw NetworkException.timeout(technicalDetails: 'LongPollChat $uri: $e');
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'LongPollChat $uri: ${e.message}');
|
||||
} on http.ClientException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'LongPollChat $uri: ${e.message}');
|
||||
}
|
||||
.timeout(Duration(seconds: timeoutSeconds + 15)),
|
||||
))!;
|
||||
|
||||
final status = response.statusCode;
|
||||
if (status == 304) return null;
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../api_params.dart';
|
||||
import '../../api_request.dart';
|
||||
import '../../api_response.dart';
|
||||
import '../../errors/auth_exception.dart';
|
||||
import '../../errors/network_exception.dart';
|
||||
import '../../errors/not_found_exception.dart';
|
||||
import '../../errors/parse_exception.dart';
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../../http_errors.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
|
||||
enum TalkApiMethod { get, post, put, delete }
|
||||
|
||||
abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
||||
abstract class TalkApi<T extends ApiResponse?> {
|
||||
String path;
|
||||
ApiParams? body;
|
||||
Map<String, String>? headers;
|
||||
Map<String, dynamic>? getParameters;
|
||||
|
||||
http.Response? response;
|
||||
|
||||
TalkApi(this.path, this.body, {this.headers, this.getParameters});
|
||||
|
||||
Future<http.Response>? request(
|
||||
@@ -40,22 +31,15 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
||||
);
|
||||
final mergedHeaders = {...NextcloudOcs.headers(), ...?headers};
|
||||
|
||||
final http.Response data;
|
||||
try {
|
||||
final raw = await request(endpoint, body, mergedHeaders);
|
||||
if (raw == null) {
|
||||
throw const NetworkException(
|
||||
userMessage: 'Keine Antwort vom Talk-Server erhalten.',
|
||||
technicalDetails: 'Talk request returned null',
|
||||
);
|
||||
}
|
||||
data = raw;
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Talk $endpoint: ${e.message}');
|
||||
} on TimeoutException catch (e) {
|
||||
throw NetworkException.timeout(technicalDetails: 'Talk $endpoint: $e');
|
||||
} on http.ClientException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Talk $endpoint: ${e.message}');
|
||||
final data = await sendGuarded(
|
||||
'Talk $endpoint',
|
||||
() => request(endpoint, body, mergedHeaders),
|
||||
);
|
||||
if (data == null) {
|
||||
throw const NetworkException(
|
||||
userMessage: 'Keine Antwort vom Talk-Server erhalten.',
|
||||
technicalDetails: 'Talk request returned null',
|
||||
);
|
||||
}
|
||||
|
||||
final status = data.statusCode;
|
||||
@@ -63,20 +47,9 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
||||
// Talk's OCS errors carry the real reason in the body (expired session,
|
||||
// removed participant, ...); include a trimmed preview so the dialog and
|
||||
// logs surface the cause instead of just the bare status code.
|
||||
final body = data.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
||||
final detail = body.isEmpty
|
||||
? 'Talk $endpoint -> HTTP $status'
|
||||
: 'Talk $endpoint -> HTTP $status body=$preview';
|
||||
final detail = httpErrorDetail('Talk $endpoint', data.body, status);
|
||||
log(detail);
|
||||
if (status == 401) {
|
||||
throw AuthException.unauthorized(technicalDetails: detail);
|
||||
}
|
||||
if (status == 403) {
|
||||
throw AuthException.forbidden(technicalDetails: detail);
|
||||
}
|
||||
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
||||
throw ServerException(statusCode: status, technicalDetails: detail);
|
||||
throwForStatus(status, detail);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,10 +2,9 @@ import 'package:nextcloud/nextcloud.dart';
|
||||
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../api_request.dart';
|
||||
import '../../api_response.dart';
|
||||
|
||||
abstract class WebdavApi<T> extends ApiRequest {
|
||||
abstract class WebdavApi<T> {
|
||||
T genericParams;
|
||||
|
||||
WebdavApi(this.genericParams) {
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../api_request.dart';
|
||||
import '../errors/network_exception.dart';
|
||||
import '../errors/parse_exception.dart';
|
||||
import '../errors/server_exception.dart';
|
||||
import '../http_errors.dart';
|
||||
|
||||
abstract class MhslApi<T> extends ApiRequest {
|
||||
abstract class MhslApi<T> {
|
||||
String subpath;
|
||||
MhslApi(this.subpath);
|
||||
|
||||
http.Response? response;
|
||||
|
||||
Future<http.Response>? request(Uri uri);
|
||||
T assemble(String raw);
|
||||
|
||||
@@ -24,22 +19,12 @@ abstract class MhslApi<T> extends ApiRequest {
|
||||
'https://mhsl.eu/marianum/marianummobile/$subpath',
|
||||
);
|
||||
|
||||
final http.Response data;
|
||||
try {
|
||||
final raw = await request(endpoint);
|
||||
if (raw == null) {
|
||||
throw const NetworkException(
|
||||
userMessage: 'Keine Antwort vom MHSL-Dienst erhalten.',
|
||||
technicalDetails: 'mhsl request returned null',
|
||||
);
|
||||
}
|
||||
data = raw;
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'mhsl $subpath: ${e.message}');
|
||||
} on TimeoutException catch (e) {
|
||||
throw NetworkException.timeout(technicalDetails: 'mhsl $subpath: $e');
|
||||
} on http.ClientException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'mhsl $subpath: ${e.message}');
|
||||
final data = await sendGuarded('mhsl $subpath', () => request(endpoint));
|
||||
if (data == null) {
|
||||
throw const NetworkException(
|
||||
userMessage: 'Keine Antwort vom MHSL-Dienst erhalten.',
|
||||
technicalDetails: 'mhsl request returned null',
|
||||
);
|
||||
}
|
||||
|
||||
if (data.statusCode > 299) {
|
||||
@@ -55,8 +40,4 @@ abstract class MhslApi<T> extends ApiRequest {
|
||||
throw ParseException(technicalDetails: 'mhsl $subpath assemble: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static String dateTimeToJson(DateTime time) =>
|
||||
Jiffy.parseFromDateTime(time).format(pattern: 'yyyy-MM-dd HH:mm:ss');
|
||||
static DateTime dateTimeFromJson(String time) => DateTime.parse(time);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user