All checks were successful
ci/woodpecker/tag/release Pipeline was successful
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>
90 lines
2.7 KiB
Dart
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);
|
|
}
|
|
}
|