diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index dee257d7..b905e29c 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 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 @@ -16,19 +16,27 @@ * along with this program. If not, see . */ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math' as math; import 'package:http/http.dart' as http; import 'package:http/http.dart'; +import 'package:logging/logging.dart'; import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/providers/helpers.dart'; +/// initial delay for fetch retries, in milliseconds +const FETCH_INITIAL_DELAY = 250; + /// Base provider class. /// /// Provides a couple of comfort functions so we avoid a bit of boilerplate. class WgerBaseProvider { + final _logger = Logger('WgerBaseProvider'); + AuthProvider auth; late http.Client client; @@ -56,21 +64,53 @@ class WgerBaseProvider { } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response - Future fetch(Uri uri) async { - // Future | List> fetch(Uri uri) async { - // Send the request - final response = await client.get( - uri, - headers: getDefaultHeaders(includeAuth: true), - ); + /// with a simple retry mechanism for transient errors. + Future fetch( + Uri uri, { + int maxRetries = 3, + Duration initialDelay = const Duration(milliseconds: 250), + }) async { + int attempt = 0; + final random = math.Random(); - // Something wrong with our request - if (response.statusCode >= 400) { - throw WgerHttpException(response); + Future wait(String reason) async { + final backoff = (initialDelay.inMilliseconds * math.pow(2, attempt - 1)).toInt(); + final jitter = random.nextInt((backoff * 0.25).toInt() + 1); // up to 25% jitter + final delay = backoff + jitter; + _logger.info('Retrying fetch for $uri, attempt $attempt (${delay}ms), reason: $reason'); + + await Future.delayed(Duration(milliseconds: delay)); } - // Process the response - return json.decode(utf8.decode(response.bodyBytes)) as dynamic; + while (true) { + try { + final response = await client + .get(uri, headers: getDefaultHeaders(includeAuth: true)) + .timeout(const Duration(seconds: 5)); + + if (response.statusCode >= 400) { + // Retry on server errors (5xx); e.g. 502 might be transient + if (response.statusCode >= 500 && attempt < maxRetries) { + attempt++; + await wait('status code ${response.statusCode}'); + continue; + } + throw WgerHttpException(response); + } + + return json.decode(utf8.decode(response.bodyBytes)) as dynamic; + } catch (e) { + final isRetryable = + e is SocketException || e is http.ClientException || e is TimeoutException; + if (isRetryable && attempt < maxRetries) { + attempt++; + await wait(e.toString()); + continue; + } + + rethrow; + } + } } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index db2394bd..699e32d1 100644 --- a/test/core/settings_test.mocks.dart +++ b/test/core/settings_test.mocks.dart @@ -1100,9 +1100,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i18.Future fetch(Uri? uri) => + _i18.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i18.Future.value(), ) as _i18.Future); diff --git a/test/exercises/contribute_exercise_image_test.mocks.dart b/test/exercises/contribute_exercise_image_test.mocks.dart index c3685ed3..b477e9d3 100644 --- a/test/exercises/contribute_exercise_image_test.mocks.dart +++ b/test/exercises/contribute_exercise_image_test.mocks.dart @@ -402,9 +402,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i14.Future fetch(Uri? uri) => + _i14.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i14.Future.value(), ) as _i14.Future); diff --git a/test/gallery/gallery_form_test.mocks.dart b/test/gallery/gallery_form_test.mocks.dart index e14d7838..3ec16dfa 100644 --- a/test/gallery/gallery_form_test.mocks.dart +++ b/test/gallery/gallery_form_test.mocks.dart @@ -175,9 +175,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri) => + _i6.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i6.Future.value(), ) as _i6.Future); diff --git a/test/gallery/gallery_screen_test.mocks.dart b/test/gallery/gallery_screen_test.mocks.dart index d1ab1318..c9fe9e74 100644 --- a/test/gallery/gallery_screen_test.mocks.dart +++ b/test/gallery/gallery_screen_test.mocks.dart @@ -175,9 +175,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri) => + _i6.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i6.Future.value(), ) as _i6.Future); diff --git a/test/measurements/measurement_provider_test.mocks.dart b/test/measurements/measurement_provider_test.mocks.dart index ef362a87..96f42b39 100644 --- a/test/measurements/measurement_provider_test.mocks.dart +++ b/test/measurements/measurement_provider_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/nutrition/nutritional_plan_screen_test.mocks.dart b/test/nutrition/nutritional_plan_screen_test.mocks.dart index 2fe3c4ac..3ee7049a 100644 --- a/test/nutrition/nutritional_plan_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plan_screen_test.mocks.dart @@ -122,9 +122,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/nutrition/nutritional_plans_screen_test.mocks.dart b/test/nutrition/nutritional_plans_screen_test.mocks.dart index c702d401..5c9dae55 100644 --- a/test/nutrition/nutritional_plans_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plans_screen_test.mocks.dart @@ -357,9 +357,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/providers/base_provider.dart b/test/providers/base_provider.dart new file mode 100644 index 00000000..203973c3 --- /dev/null +++ b/test/providers/base_provider.dart @@ -0,0 +1,135 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 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'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/providers/base_provider.dart'; + +import '../utils.dart'; +import 'base_provider.mocks.dart'; + +@GenerateMocks([Client]) +void main() { + final Uri testUri = Uri(scheme: 'https', host: 'localhost', path: 'api/v2/test/'); + + test('Retry on SocketException then succeeds', () async { + // Arrange + final mockClient = MockClient(); + var callCount = 0; + when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) { + if (callCount == 0) { + callCount++; + return Future.error(const SocketException('conn fail')); + } + return Future.value(Response('{"ok": true}', 200)); + }); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2); + }); + + test('Retry on 5xx then succeeds', () async { + // Arrange + final mockClient = MockClient(); + var callCount = 0; + when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) { + if (callCount == 0) { + callCount++; + return Future.value(Response('{"msg":"error"}', 502)); + } + return Future.value(Response('{"ok": true}', 200)); + }); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2); + }); + + test('Do not retry on 4xx client error', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.value(Response('{"error":"bad"}', 400))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + + // Assert + await expectLater( + provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)), + throwsA(isA()), + ); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1); + }); + + test('Exceed max retries and rethrow after retries', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.error(ClientException('conn fail'))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + dynamic caught; + try { + await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + } catch (e) { + caught = e; + } + + // Assert + expect(caught, isA()); + // initial try + 3 retries = 4 calls + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(4); + }); + + test('Request succeeds without retries', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.value(Response('{"ok": true}', 200))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1); + }); +} diff --git a/test/providers/base_provider.mocks.dart b/test/providers/base_provider.mocks.dart new file mode 100644 index 00000000..5111102b --- /dev/null +++ b/test/providers/base_provider.mocks.dart @@ -0,0 +1,218 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/providers/base_provider.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i6; + +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake implements _i2.StreamedResponse { + _FakeStreamedResponse_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.Response> head(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#head, [url], {#headers: headers}), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method(#head, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> get(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#get, [url], {#headers: headers}), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method(#get, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #post, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #put, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future read(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#read, [url], {#headers: headers}), + returnValue: _i3.Future.value( + _i5.dummyValue( + this, + Invocation.method(#read, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future); + + @override + _i3.Future<_i6.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method(#readBytes, [url], {#headers: headers}), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) + as _i3.Future<_i6.Uint8List>); + + @override + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method(#send, [request]), + returnValue: _i3.Future<_i2.StreamedResponse>.value( + _FakeStreamedResponse_1( + this, + Invocation.method(#send, [request]), + ), + ), + ) + as _i3.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method(#close, []), + returnValueForMissingStub: null, + ); +} diff --git a/test/routine/gym_mode/gym_mode_test.mocks.dart b/test/routine/gym_mode/gym_mode_test.mocks.dart index 6b3c882c..81002948 100644 --- a/test/routine/gym_mode/gym_mode_test.mocks.dart +++ b/test/routine/gym_mode/gym_mode_test.mocks.dart @@ -201,9 +201,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i20.Future fetch(Uri? uri) => + _i20.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i20.Future.value(), ) as _i20.Future); diff --git a/test/routine/routine_screen_test.mocks.dart b/test/routine/routine_screen_test.mocks.dart index 99fe1643..41f40bb3 100644 --- a/test/routine/routine_screen_test.mocks.dart +++ b/test/routine/routine_screen_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/routine/routines_provider_test.mocks.dart b/test/routine/routines_provider_test.mocks.dart index 06326506..f9125d59 100644 --- a/test/routine/routines_provider_test.mocks.dart +++ b/test/routine/routines_provider_test.mocks.dart @@ -151,9 +151,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i11.Future fetch(Uri? uri) => + _i11.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i11.Future.value(), ) as _i11.Future); diff --git a/test/routine/routines_screen_test.mocks.dart b/test/routine/routines_screen_test.mocks.dart index 2607f048..ea1014f7 100644 --- a/test/routine/routines_screen_test.mocks.dart +++ b/test/routine/routines_screen_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index 08bbf636..ecf42fc8 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/weight/weight_provider_test.mocks.dart b/test/weight/weight_provider_test.mocks.dart index 4cd18bb5..5fc97437 100644 --- a/test/weight/weight_provider_test.mocks.dart +++ b/test/weight/weight_provider_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future);