fixed stalled requests after app resume

This commit is contained in:
2026-09-24 22:33:00 +02:00
parent 4f31e68456
commit de866b12b4
5 changed files with 34 additions and 7 deletions
@@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
import '../../../model/endpoint_data.dart'; import '../../../model/endpoint_data.dart';
import '../../errors/server_exception.dart'; import '../../errors/server_exception.dart';
import '../../http_errors.dart';
import '../nextcloud_ocs.dart'; import '../nextcloud_ocs.dart';
import 'autocomplete_response.dart'; import 'autocomplete_response.dart';
@@ -30,7 +31,12 @@ class AutocompleteApi {
'limit': '10', 'limit': '10',
}, },
); );
final response = await http.get(uri, headers: NextcloudOcs.headers()); final response = (await sendGuarded(
'Autocomplete $uri',
() => http
.get(uri, headers: NextcloudOcs.headers())
.timeout(const Duration(seconds: 20)),
))!;
if (response.statusCode != HttpStatus.ok) { if (response.statusCode != HttpStatus.ok) {
throw ServerException( throw ServerException(
statusCode: response.statusCode, statusCode: response.statusCode,
+5 -1
View File
@@ -12,6 +12,10 @@ import '../nextcloud_ocs.dart';
import 'talk_error.dart'; import 'talk_error.dart';
abstract class TalkApi<T extends ApiResponse?> { abstract class TalkApi<T extends ApiResponse?> {
// package:http has no timeout of its own; a request stuck on a dead
// connection (e.g. one started right before a suspend) would spin forever.
static const Duration _timeout = Duration(seconds: 30);
String path; String path;
ApiParams? body; ApiParams? body;
Map<String, String>? headers; Map<String, String>? headers;
@@ -35,7 +39,7 @@ abstract class TalkApi<T extends ApiResponse?> {
final data = await sendGuarded( final data = await sendGuarded(
'Talk $endpoint', 'Talk $endpoint',
() => request(endpoint, body, mergedHeaders), () => request(endpoint, body, mergedHeaders)?.timeout(_timeout),
); );
if (data == null) { if (data == null) {
throw const NetworkException( throw const NetworkException(
@@ -16,6 +16,16 @@ class MarianumConnectApi {
static Dio dio() => _instance; static Dio dio() => _instance;
/// Drops the pooled keep-alive sockets. After a suspend the OS or a NAT
/// has usually killed them already, but dart:io only notices once a request
/// is written into one — which then hangs until [_receiveTimeout].
/// In-flight requests finish on the old adapter.
static void resetConnections() {
final stale = _instance.httpClientAdapter;
_instance.httpClientAdapter = HttpClientAdapter();
stale.close();
}
/// A fresh dio with the standard JSON options but no interceptors — used by /// A fresh dio with the standard JSON options but no interceptors — used by
/// the auth queries (login/verify) that must bypass the bearer/demo /// the auth queries (login/verify) that must bypass the bearer/demo
/// interceptors to avoid a re-auth loop. /// interceptors to avoid a re-auth loop.
+2
View File
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'api/marianumconnect/marianumconnect_api.dart';
import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart'; import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart'; import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
import 'main.dart'; import 'main.dart';
@@ -98,6 +99,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
log('AppLifecycle: $state'); log('AppLifecycle: $state');
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
MarianumConnectApi.resetConnections();
_reportTelemetry(); _reportTelemetry();
Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () { Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () {
if (!mounted) return; if (!mounted) return;
+10 -5
View File
@@ -23,6 +23,9 @@ class ChatBloc
final ChatListBloc? _chatListBloc; final ChatListBloc? _chatListBloc;
String? _pollingToken; String? _pollingToken;
// A loop parked in its request survives stop+start for the same token
// (pause/resume); the generation retires it so only one loop polls.
int _pollGeneration = 0;
int _backoffMs = 0; int _backoffMs = 0;
int _lastKnownMessageId = 0; int _lastKnownMessageId = 0;
bool _appResumed = true; bool _appResumed = true;
@@ -265,23 +268,25 @@ class ChatBloc
_pollingToken = token; _pollingToken = token;
_backoffMs = 0; _backoffMs = 0;
_lastKnownMessageId = _maxMessageId(innerState?.chatResponse); _lastKnownMessageId = _maxMessageId(innerState?.chatResponse);
unawaited(_pollLoop(token)); unawaited(_pollLoop(token, ++_pollGeneration));
} }
void _stopLongPoll() { void _stopLongPoll() {
_pollingToken = null; _pollingToken = null;
_pollGeneration++;
_backoffMs = 0; _backoffMs = 0;
} }
Future<void> _pollLoop(String token) async { Future<void> _pollLoop(String token, int generation) async {
while (_pollingToken == token && !isClosed) { bool active() => generation == _pollGeneration && !isClosed;
while (active()) {
try { try {
final response = await LongPollChat( final response = await LongPollChat(
chatToken: token, chatToken: token,
lastKnownMessageId: _lastKnownMessageId, lastKnownMessageId: _lastKnownMessageId,
).run(); ).run();
if (_pollingToken != token || isClosed) return; if (!active()) return;
_backoffMs = 0; _backoffMs = 0;
if (response == null) continue; if (response == null) continue;
@@ -305,7 +310,7 @@ class ChatBloc
_chatListBloc?.markRoomAsRead(token, _lastKnownMessageId); _chatListBloc?.markRoomAsRead(token, _lastKnownMessageId);
} }
} on Object catch (e) { } on Object catch (e) {
if (_pollingToken != token || isClosed) return; if (!active()) return;
log('LongPoll error for $token: $e'); log('LongPoll error for $token: $e');
_backoffMs = _backoffMs == 0 ? 2000 : math.min(_backoffMs * 2, 30000); _backoffMs = _backoffMs == 0 ? 2000 : math.min(_backoffMs * 2, 30000);
await Future.delayed(Duration(milliseconds: _backoffMs)); await Future.delayed(Duration(milliseconds: _backoffMs));