worldhopper/lib/services/dio_factory.dart
Felipe M. d2497f8aed
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
feat: add background download service with error handling improvements
Add Android foreground service support for downloads to prevent OS
process killing during long operations. Fix multiple error handling
issues: foreground service leak on exceptions, polling stuck forever
on failure, double-counting chapters on retry, silent retry failures
without user feedback, and app crash if background service init fails.
Replace inaccurate failed chapter count heuristic with actual data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 11:44:37 +01:00

90 lines
2.7 KiB
Dart

import 'package:dio/dio.dart';
/// Creates Dio instances that automatically follow redirects for all HTTP
/// methods (including POST). The default Dio/dart:io behaviour only follows
/// redirects for GET, so POST→301/302 would otherwise throw.
Dio createDio([BaseOptions? options]) {
final dio = Dio(options);
dio.interceptors.add(_RedirectInterceptor());
return dio;
}
/// Interceptor that follows 3xx redirects for any HTTP method.
class _RedirectInterceptor extends Interceptor {
static const _maxRedirects = 5;
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
final statusCode = err.response?.statusCode;
if (statusCode == null ||
statusCode < 300 ||
statusCode > 399 ||
statusCode == 304) {
return handler.next(err);
}
final location = err.response?.headers.value('location');
if (location == null) {
return handler.next(err);
}
final originalUri = err.requestOptions.uri;
final redirectUri = Uri.parse(location).isAbsolute
? Uri.parse(location)
: originalUri.resolve(location);
// Preserve method for 307/308, change to GET for 301/302/303
final preserveMethod = statusCode == 307 || statusCode == 308;
final method = preserveMethod ? err.requestOptions.method : 'GET';
final dio = Dio();
dio.interceptors.add(this); // follow further redirects
var redirects = 0;
var opts = Options(
method: method,
headers: Map.of(err.requestOptions.headers),
responseType: err.requestOptions.responseType,
validateStatus: err.requestOptions.validateStatus,
followRedirects: false, // we handle it ourselves
);
var currentUri = redirectUri;
Response? response;
while (redirects < _maxRedirects) {
redirects++;
try {
response = await dio.requestUri(
currentUri,
data: preserveMethod ? err.requestOptions.data : null,
options: opts,
);
break;
} on DioException catch (e) {
final sc = e.response?.statusCode;
final loc = e.response?.headers.value('location');
if (sc != null && sc >= 300 && sc <= 399 && sc != 304 && loc != null) {
currentUri = Uri.parse(loc).isAbsolute
? Uri.parse(loc)
: currentUri.resolve(loc);
if (sc == 301 || sc == 302 || sc == 303) {
opts = opts.copyWith(method: 'GET');
}
continue;
}
return handler.next(e);
}
}
if (response != null) {
return handler.resolve(response);
}
return handler.next(err);
}
}