fix(flutter): cover URLs send Bearer auth + audio handler diag logging

Two issues from on-device testing against prod HTTPS:

1. ServerImage was resolving cover URLs correctly
   (https://minstrel.fabledsword.com/api/albums/.../cover) but the server
   returned 401 because Image.network doesn't carry the session token
   automatically the way the browser sends cookies. Forwards the stored
   session token as an Authorization: Bearer header. No-op when no token
   is present.

2. The "Cleartext HTTP traffic to 127.0.0.1" audio error reproduced even
   after the previous defensive check landed, which means the URL handed
   to ExoPlayer has a valid scheme+host (just the wrong host). The check
   only catches scheme-less URLs, so it didn't fire. Added debugPrint
   logging at configure() and setQueueFromTracks() time to show the
   actual baseUrl + per-track resolved URL on the next reproduction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 23:15:56 -04:00
parent 088e8f4bf8
commit 83df3773ae
2 changed files with 16 additions and 1 deletions
@@ -1,4 +1,5 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/foundation.dart';
import 'package:just_audio/just_audio.dart';
import '../models/track.dart';
@@ -15,6 +16,8 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
void configure({required String baseUrl, required String? token}) {
_baseUrl = baseUrl;
_token = token;
debugPrint('audio_handler.configure: baseUrl="$baseUrl" '
'tokenPresent=${token != null && token.isNotEmpty}');
}
Future<void> setQueueFromTracks(List<TrackRef> tracks, {int initialIndex = 0}) async {
@@ -25,8 +28,12 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
}
final headers = _token == null ? null : {'Authorization': 'Bearer $_token'};
debugPrint('audio_handler.setQueueFromTracks: '
'_baseUrl="$_baseUrl" trackCount=${tracks.length}');
final sources = tracks.map((t) {
final url = _resolveStreamUrl(t);
debugPrint('audio_handler: track.id=${t.id} '
'track.streamUrl="${t.streamUrl}" → resolved="$url"');
// Defensive: a scheme-less URL handed to ExoPlayer crashes with
// a confusing "Cleartext HTTP traffic to 127.0.0.1 not permitted"
// error because Android's URL parser falls back to localhost.
@@ -31,7 +31,15 @@ class ServerImage extends ConsumerWidget {
final base = ref.watch(serverUrlProvider).value;
final resolved = _resolve(base, url);
if (resolved == null) return empty;
return Image.network(resolved, fit: fit);
// Cover endpoints are gated by RequireUser server-side. Image.network
// doesn't carry cookies/Bearer tokens automatically the way the
// browser's <img> tag does, so we forward the session token as a
// header. Without this, the server returns 401 and the image fails.
final token = ref.watch(sessionTokenProvider).value;
final headers = (token != null && token.isNotEmpty)
? {'Authorization': 'Bearer $token'}
: null;
return Image.network(resolved, fit: fit, headers: headers);
}
static String? _resolve(String? baseUrl, String url) {