From 83df3773aec5228b23710062e22573cc26d9ef8c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 8 May 2026 23:15:56 -0400 Subject: [PATCH] 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) --- flutter_client/lib/player/audio_handler.dart | 7 +++++++ flutter_client/lib/shared/widgets/server_image.dart | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index 657e5b4c..13886545 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -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 setQueueFromTracks(List 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. diff --git a/flutter_client/lib/shared/widgets/server_image.dart b/flutter_client/lib/shared/widgets/server_image.dart index 6559ac9d..2b8ca417 100644 --- a/flutter_client/lib/shared/widgets/server_image.dart +++ b/flutter_client/lib/shared/widgets/server_image.dart @@ -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 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) {