From 077dcaf742f69d99c332a53a7d358ee999457368 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Wed, 17 Dec 2025 15:14:47 +0100 Subject: [PATCH] Handle HTML errors in WgerHttpException These need to be handled separately when the server encounters an error and returns HTML instead of JSON. --- lib/core/exceptions/http_exception.dart | 85 +++++++++++++++++ .../exceptions/no_result_exception.dart | 0 .../exceptions/no_such_entry_exception.dart | 0 lib/exceptions/http_exception.dart | 48 ---------- lib/helpers/errors.dart | 94 ++++++++++++------- lib/main.dart | 2 +- .../measurements/measurement_category.dart | 2 +- lib/providers/auth.dart | 17 ++-- lib/providers/base_provider.dart | 10 +- lib/providers/body_weight.dart | 4 +- lib/providers/exercises.dart | 2 +- lib/providers/measurement.dart | 4 +- lib/providers/nutrition.dart | 10 +- lib/providers/routines.dart | 4 +- lib/screens/add_exercise_screen.dart | 2 +- lib/screens/auth_screen.dart | 2 +- lib/screens/measurement_entries_screen.dart | 6 +- lib/widgets/exercises/videos.dart | 20 ++-- lib/widgets/routines/forms/day.dart | 2 +- lib/widgets/routines/forms/routine.dart | 2 +- lib/widgets/routines/forms/session.dart | 2 +- lib/widgets/routines/forms/slot.dart | 2 +- lib/widgets/routines/forms/slot_entry.dart | 2 +- lib/widgets/routines/gym_mode/log_page.dart | 2 +- test/core/http_exception_test.dart | 77 +++++++++++++++ test/exercises/contribute_exercise_test.dart | 4 +- test/exercises/exercise_provider_test.dart | 2 +- test/{utils => helpers}/errors_test.dart | 0 .../measurement_category_test.dart | 2 +- .../measurement_provider_test.dart | 10 +- test/routine/forms/session_form_test.dart | 2 +- test/routine/routine_form_test.dart | 2 +- 32 files changed, 284 insertions(+), 139 deletions(-) create mode 100644 lib/core/exceptions/http_exception.dart rename lib/{ => core}/exceptions/no_result_exception.dart (100%) rename lib/{ => core}/exceptions/no_such_entry_exception.dart (100%) delete mode 100644 lib/exceptions/http_exception.dart create mode 100644 test/core/http_exception_test.dart rename test/{utils => helpers}/errors_test.dart (100%) diff --git a/lib/core/exceptions/http_exception.dart b/lib/core/exceptions/http_exception.dart new file mode 100644 index 00000000..948b8975 --- /dev/null +++ b/lib/core/exceptions/http_exception.dart @@ -0,0 +1,85 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2020 - 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart'; + +enum ErrorType { + json, + html, + text, +} + +const HTML_ERROR_KEY = 'html_error'; + +class WgerHttpException implements Exception { + Map errors = {}; + + /// The exception type. While the majority will be json, it is possible that + /// the server will return HTML, e.g. if there has been an internal server error + /// or similar. + late ErrorType type; + + /// Custom http exception + WgerHttpException(Response response) { + type = ErrorType.json; + final dynamic responseBody = response.body; + + final contentType = response.headers[HttpHeaders.contentTypeHeader]; + if (contentType != null && contentType.contains('text/html')) { + type = ErrorType.html; + } + + if (responseBody == null) { + errors = {'unknown_error': 'An unknown error occurred, no further information available'}; + } else { + try { + if (type == ErrorType.json) { + final response = json.decode(responseBody); + errors = (response is Map ? response : {'unknown_error': response}) + .cast(); + } else if (type == ErrorType.html) { + errors = {HTML_ERROR_KEY: responseBody.toString()}; + } else { + errors = {'text_error': responseBody.toString()}; + } + } catch (e) { + errors = {'unknown_error': responseBody}; + } + } + } + + WgerHttpException.fromMap(Map map) : type = ErrorType.json { + errors = map; + } + + String get htmlError { + if (type != ErrorType.html) { + return ''; + } + + return errors[HTML_ERROR_KEY] ?? ''; + } + + @override + String toString() { + return 'WgerHttpException ($type): $errors'; + } +} diff --git a/lib/exceptions/no_result_exception.dart b/lib/core/exceptions/no_result_exception.dart similarity index 100% rename from lib/exceptions/no_result_exception.dart rename to lib/core/exceptions/no_result_exception.dart diff --git a/lib/exceptions/no_such_entry_exception.dart b/lib/core/exceptions/no_such_entry_exception.dart similarity index 100% rename from lib/exceptions/no_such_entry_exception.dart rename to lib/core/exceptions/no_such_entry_exception.dart diff --git a/lib/exceptions/http_exception.dart b/lib/exceptions/http_exception.dart deleted file mode 100644 index 06cd2915..00000000 --- a/lib/exceptions/http_exception.dart +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import 'dart:convert'; - -class WgerHttpException implements Exception { - Map errors = {}; - - /// Custom http exception. - /// Expects the response body of the REST call and will try to parse it to - /// JSON. Will use the response as-is if it fails. - WgerHttpException(dynamic responseBody) { - if (responseBody == null) { - errors = {'unknown_error': 'An unknown error occurred, no further information available'}; - } else { - try { - final response = json.decode(responseBody); - errors = (response is Map ? response : {'unknown_error': response}).cast(); - } catch (e) { - errors = {'unknown_error': responseBody}; - } - } - } - - WgerHttpException.fromMap(Map map) { - errors = map; - } - - @override - String toString() { - return errors.values.toList().join(', '); - } -} diff --git a/lib/helpers/errors.dart b/lib/helpers/errors.dart index 77d03a0a..926c589d 100644 --- a/lib/helpers/errors.dart +++ b/lib/helpers/errors.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -22,11 +22,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/main.dart'; import 'package:wger/models/workouts/log.dart'; @@ -50,16 +51,23 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, {BuildContext? co return; } - final errorList = formatApiErrors(extractErrors(exception.errors)); + final theme = Theme.of(dialogContext); showDialog( context: dialogContext, builder: (ctx) => AlertDialog( title: Text(AppLocalizations.of(ctx).anErrorOccurred), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [...errorList], + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (exception.type == ErrorType.html) + ServerHtmlError(data: exception.htmlError) + else + ...formatApiErrors(extractErrors(exception.errors)), + ], + ), ), actions: [ TextButton( @@ -145,7 +153,10 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext tilePadding: EdgeInsets.zero, title: Text(i18n.errorViewDetails), children: [ - Text(issueErrorMessage, style: const TextStyle(fontWeight: FontWeight.bold)), + Text( + issueErrorMessage, + style: const TextStyle(fontWeight: FontWeight.bold), + ), Container( alignment: Alignment.topLeft, padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -237,6 +248,31 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext ); } +/// A widget to render HTML errors returned by the server +/// +/// This is a simple wrapper around the `Html` Widget, with some light changes +/// to the style. +class ServerHtmlError extends StatelessWidget { + final logger = Logger('ServerHtml'); + final String data; + + ServerHtmlError({required this.data, super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Html( + data: data, + style: { + 'h1': Style(fontSize: FontSize(theme.textTheme.bodyLarge?.fontSize ?? 15)), + 'h2': Style(fontSize: FontSize(theme.textTheme.bodyMedium?.fontSize ?? 15)), + }, + doNotRenderTheseTags: const {'a'}, + ); + } +} + class CopyToClipboardButton extends StatelessWidget { final logger = Logger('CopyToClipboardButton'); final String text; @@ -423,38 +459,26 @@ class FormHttpErrorsWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( constraints: const BoxConstraints(maxHeight: 250), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.error, width: 1), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.all(10), child: SingleChildScrollView( child: Column( children: [ - Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error), - ...formatApiErrors( - extractErrors(exception.errors), - color: Theme.of(context).colorScheme.error, - ), - ], - ), - ), - ); - } -} - -class GeneralErrorsWidget extends StatelessWidget { - final String? title; - final List widgets; - - const GeneralErrorsWidget(this.widgets, {this.title, super.key}); - - @override - Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxHeight: 250), - child: SingleChildScrollView( - child: Column( - children: [ - Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error), - ...formatTextErrors(widgets, title: title, color: Theme.of(context).colorScheme.error), + Icon(Icons.error_outline, color: theme.colorScheme.error), + if (exception.type == ErrorType.html) + ServerHtmlError(data: exception.htmlError) + else + ...formatApiErrors( + extractErrors(exception.errors), + color: theme.colorScheme.error, + ), ], ), ), diff --git a/lib/main.dart b/lib/main.dart index db8836c1..47761881 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,8 +21,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/core/locator.dart'; -import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/models/measurements/measurement_category.dart b/lib/models/measurements/measurement_category.dart index d32925db..e5c8cdfd 100644 --- a/lib/models/measurements/measurement_category.dart +++ b/lib/models/measurements/measurement_category.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; part 'measurement_category.g.dart'; diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index a00a292a..edb73a8d 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -27,7 +27,7 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:version/version.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; @@ -132,7 +132,7 @@ class AuthProvider with ChangeNotifier { ); if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return login(username, password, serverUrl, null); @@ -158,8 +158,8 @@ class AuthProvider with ChangeNotifier { }, ); - if (response.statusCode != 200) { - throw WgerHttpException(response.body); + if (response.statusCode >= 400) { + throw WgerHttpException(response); } token = apiToken; @@ -169,17 +169,18 @@ class AuthProvider with ChangeNotifier { final response = await client.post( makeUri(serverUrl, LOGIN_URL), headers: { - HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8', HttpHeaders.userAgentHeader: getAppNameHeader(), }, body: json.encode({'username': username, 'password': password}), ); - final responseData = json.decode(response.body); if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } + final responseData = json.decode(response.body); + token = responseData['token']; } diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index 351efe99..dee257d7 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -21,7 +21,7 @@ import 'dart:io'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/providers/helpers.dart'; @@ -66,7 +66,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } // Process the response @@ -104,7 +104,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return json.decode(response.body); @@ -120,7 +120,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return json.decode(response.body); @@ -137,7 +137,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return response; } diff --git a/lib/providers/body_weight.dart b/lib/providers/body_weight.dart index dddb8cec..cc93f7c3 100644 --- a/lib/providers/body_weight.dart +++ b/lib/providers/body_weight.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/body_weight/weight_entry.dart'; import 'package:wger/providers/base_provider.dart'; @@ -115,7 +115,7 @@ class BodyWeightProvider with ChangeNotifier { if (response.statusCode >= 400) { _entries.insert(existingEntryIndex, existingWeightEntry); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } } diff --git a/lib/providers/exercises.dart b/lib/providers/exercises.dart index 5ab3d70f..50c8175c 100644 --- a/lib/providers/exercises.dart +++ b/lib/providers/exercises.dart @@ -22,9 +22,9 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/locator.dart'; import 'package:wger/database/exercises/exercise_database.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/category.dart'; diff --git a/lib/providers/measurement.dart b/lib/providers/measurement.dart index c961e938..6c0292f0 100644 --- a/lib/providers/measurement.dart +++ b/lib/providers/measurement.dart @@ -18,8 +18,8 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 11a82485..3ec50f80 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -21,10 +21,10 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/locator.dart'; import 'package:wger/database/ingredients/ingredients_database.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/ingredient_image.dart'; @@ -220,7 +220,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { _plans.insert(existingPlanIndex, existingPlan); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } //existingPlan = null; } @@ -263,7 +263,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { plan.meals.insert(mealIndex, existingMeal); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } @@ -293,7 +293,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { meal.mealItems.insert(mealItemIndex, existingMealItem); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } diff --git a/lib/providers/routines.dart b/lib/providers/routines.dart index ec8e53b9..56d163dc 100644 --- a/lib/providers/routines.dart +++ b/lib/providers/routines.dart @@ -20,7 +20,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/exercise.dart'; @@ -374,7 +374,7 @@ class RoutinesProvider with ChangeNotifier { if (response.statusCode >= 400) { _routines.insert(routineIndex, routine); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index c91f74ef..9d94f5d2 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/core/wide_screen_wrapper.dart'; -import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 902018df..aaa55e46 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -19,7 +19,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/screens/measurement_entries_screen.dart b/lib/screens/measurement_entries_screen.dart index 550a3238..3f30ed3e 100644 --- a/lib/screens/measurement_entries_screen.dart +++ b/lib/screens/measurement_entries_screen.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -18,8 +18,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/wide_screen_wrapper.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/providers/measurement.dart'; diff --git a/lib/widgets/exercises/videos.dart b/lib/widgets/exercises/videos.dart index 8f38da56..18d76ef9 100644 --- a/lib/widgets/exercises/videos.dart +++ b/lib/widgets/exercises/videos.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -20,6 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:video_player/video_player.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/models/exercises/video.dart'; @@ -33,9 +34,10 @@ class ExerciseVideoWidget extends StatefulWidget { } class _ExerciseVideoWidgetState extends State { + final logger = Logger('ExerciseVideoWidgetState'); + late VideoPlayerController _controller; bool hasError = false; - final logger = Logger('ExerciseVideoWidgetState'); @override void initState() { @@ -66,10 +68,14 @@ class _ExerciseVideoWidgetState extends State { @override Widget build(BuildContext context) { return hasError - ? const GeneralErrorsWidget( - [ - 'An error happened while loading the video. If you can, please check the application logs.', - ], + ? FormHttpErrorsWidget( + WgerHttpException.fromMap( + const { + 'error': + 'An error happened while loading the video. If you can, ' + 'please check the application logs.', + }, + ), ) : _controller.value.isInitialized ? AspectRatio( diff --git a/lib/widgets/routines/forms/day.dart b/lib/widgets/routines/forms/day.dart index a0ae3c25..6af0dc10 100644 --- a/lib/widgets/routines/forms/day.dart +++ b/lib/widgets/routines/forms/day.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/forms/routine.dart b/lib/widgets/routines/forms/routine.dart index bbdc9bb2..cb6d3b01 100644 --- a/lib/widgets/routines/forms/routine.dart +++ b/lib/widgets/routines/forms/routine.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/forms/session.dart b/lib/widgets/routines/forms/session.dart index 231adc73..57b7d7a2 100644 --- a/lib/widgets/routines/forms/session.dart +++ b/lib/widgets/routines/forms/session.dart @@ -20,7 +20,7 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/helpers/json.dart'; diff --git a/lib/widgets/routines/forms/slot.dart b/lib/widgets/routines/forms/slot.dart index 59ab4aa9..f392d997 100644 --- a/lib/widgets/routines/forms/slot.dart +++ b/lib/widgets/routines/forms/slot.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/day.dart'; diff --git a/lib/widgets/routines/forms/slot_entry.dart b/lib/widgets/routines/forms/slot_entry.dart index 94ccd7f5..a9580004 100644 --- a/lib/widgets/routines/forms/slot_entry.dart +++ b/lib/widgets/routines/forms/slot_entry.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index b9409780..d8ce9e04 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -21,7 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart' as provider; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/log.dart'; diff --git a/test/core/http_exception_test.dart b/test/core/http_exception_test.dart new file mode 100644 index 00000000..26b7a754 --- /dev/null +++ b/test/core/http_exception_test.dart @@ -0,0 +1,77 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2020 - 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:wger/core/exceptions/http_exception.dart'; + +void main() { + group('WgerHttpException', () { + test('parses valid JSON response', () { + final resp = http.Response( + '{"foo":"bar"}', + 400, + headers: {HttpHeaders.contentTypeHeader: 'application/json'}, + ); + + final ex = WgerHttpException(resp); + + expect(ex.type, ErrorType.json); + expect(ex.errors['foo'], 'bar'); + expect(ex.toString(), contains('WgerHttpException')); + }); + + test('falls back on malformed JSON', () { + const body = '{"foo":'; + final resp = http.Response( + body, + 500, + headers: {HttpHeaders.contentTypeHeader: 'application/json'}, + ); + + final ex = WgerHttpException(resp); + + expect(ex.type, ErrorType.json); + expect(ex.errors['unknown_error'], body); + }); + + test('detects HTML response', () { + const body = 'Error'; + final resp = http.Response( + body, + 500, + headers: {HttpHeaders.contentTypeHeader: 'text/html; charset=utf-8'}, + ); + + final ex = WgerHttpException(resp); + + expect(ex.type, ErrorType.html); + expect(ex.htmlError, body); + }); + + test('fromMap sets errors and type', () { + final map = {'field': 'value'}; + final ex = WgerHttpException.fromMap(map); + + expect(ex.type, ErrorType.json); + expect(ex.errors, map); + }); + }); +} diff --git a/test/exercises/contribute_exercise_test.dart b/test/exercises/contribute_exercise_test.dart index a909efc0..df1c3b4b 100644 --- a/test/exercises/contribute_exercise_test.dart +++ b/test/exercises/contribute_exercise_test.dart @@ -21,7 +21,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/exercises.dart'; @@ -422,7 +422,7 @@ void main() { testWidgets('Failed submission displays error message', (WidgetTester tester) async { // Setup: Create verified user and mock failed submission setupFullVerifiedUserContext(); - final httpException = WgerHttpException({ + final httpException = WgerHttpException.fromMap({ 'name': ['This field is required'], }); when(mockAddExerciseProvider.postExerciseToServer()).thenThrow(httpException); diff --git a/test/exercises/exercise_provider_test.dart b/test/exercises/exercise_provider_test.dart index 82a9d861..f0920635 100644 --- a/test/exercises/exercise_provider_test.dart +++ b/test/exercises/exercise_provider_test.dart @@ -7,8 +7,8 @@ import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/database/exercises/exercise_database.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/category.dart'; diff --git a/test/utils/errors_test.dart b/test/helpers/errors_test.dart similarity index 100% rename from test/utils/errors_test.dart rename to test/helpers/errors_test.dart diff --git a/test/measurements/measurement_category_test.dart b/test/measurements/measurement_category_test.dart index 21009d0e..3e16394d 100644 --- a/test/measurements/measurement_category_test.dart +++ b/test/measurements/measurement_category_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; diff --git a/test/measurements/measurement_provider_test.dart b/test/measurements/measurement_provider_test.dart index 799ac261..675f6c1b 100644 --- a/test/measurements/measurement_provider_test.dart +++ b/test/measurements/measurement_provider_test.dart @@ -4,8 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; @@ -271,7 +271,7 @@ void main() { 'should re-add the "removed" MeasurementCategory and relay the exception on WgerHttpException', () { // arrange - when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( @@ -330,7 +330,7 @@ void main() { test('should keep categories list as is on WgerHttpException', () { // arrange - when(mockWgerBaseProvider.patch(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.patch(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( @@ -550,7 +550,7 @@ void main() { ), const MeasurementCategory(id: 2, name: 'Biceps', unit: 'cm'), ]; - when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( diff --git a/test/routine/forms/session_form_test.dart b/test/routine/forms/session_form_test.dart index 53302ea9..53febec2 100644 --- a/test/routine/forms/session_form_test.dart +++ b/test/routine/forms/session_form_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/session.dart'; import 'package:wger/providers/routines.dart'; diff --git a/test/routine/routine_form_test.dart b/test/routine/routine_form_test.dart index 8f287f09..c40c7426 100644 --- a/test/routine/routine_form_test.dart +++ b/test/routine/routine_form_test.dart @@ -21,7 +21,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart';