From 6169d7a4ac0ccefe5dc0cc545f2563d470c2f956 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Sat, 7 Feb 2026 11:49:43 -0500 Subject: [PATCH] fix: session capabilities using disposed HTTP context - Extract AccessToken from auth response before background task - Create new HeaderDictionary with token instead of using Request.Headers - Prevents ObjectDisposedException when HTTP context is disposed - Session capabilities now work correctly for all clients Note: WebSocket support for external tracks already implemented via JellyfinSessionManager.EnsureSessionAsync and WebSocketProxyMiddleware --- allstarr/Controllers/JellyfinController.cs | 75 ++++++++++++++-------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index af57b0d..02f71eb 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -1838,43 +1838,62 @@ public class JellyfinController : ControllerBase { var responseJson = result.RootElement.GetRawText(); - // On successful auth, post session capabilities in background + // On successful auth, extract access token and post session capabilities in background if (statusCode == 200) { - _logger.LogInformation("Authentication successful, posting session capabilities in background"); + _logger.LogInformation("Authentication successful"); - // Don't await - do this in background so we return auth response immediately - _ = Task.Run(async () => + // Extract access token from response for session capabilities + string? accessToken = null; + if (result.RootElement.TryGetProperty("AccessToken", out var tokenEl)) { - try + accessToken = tokenEl.GetString(); + } + + // Post session capabilities in background if we have a token + if (!string.IsNullOrEmpty(accessToken)) + { + // Capture token in closure - don't use Request.Headers (will be disposed) + var token = accessToken; + _ = Task.Run(async () => { - _logger.LogInformation("🔧 Posting session capabilities after authentication"); - var capabilities = new + try { - PlayableMediaTypes = new[] { "Audio" }, - SupportedCommands = Array.Empty(), - SupportsMediaControl = false, - SupportsPersistentIdentifier = true, - SupportsSync = false - }; - - var capabilitiesJson = JsonSerializer.Serialize(capabilities); - var (capResult, capStatus) = await _proxyService.PostJsonAsync("Sessions/Capabilities/Full", capabilitiesJson, Request.Headers); - - if (capStatus == 204 || capStatus == 200) - { - _logger.LogInformation("✓ Session capabilities posted after auth ({StatusCode})", capStatus); + _logger.LogDebug("🔧 Posting session capabilities after authentication"); + + // Build auth header with the new token + var authHeaders = new HeaderDictionary + { + ["X-Emby-Token"] = token + }; + + var capabilities = new + { + PlayableMediaTypes = new[] { "Audio" }, + SupportedCommands = Array.Empty(), + SupportsMediaControl = false, + SupportsPersistentIdentifier = true, + SupportsSync = false + }; + + var capabilitiesJson = JsonSerializer.Serialize(capabilities); + var (capResult, capStatus) = await _proxyService.PostJsonAsync("Sessions/Capabilities/Full", capabilitiesJson, authHeaders); + + if (capStatus == 204 || capStatus == 200) + { + _logger.LogDebug("✓ Session capabilities posted after auth ({StatusCode})", capStatus); + } + else + { + _logger.LogDebug("⚠ Session capabilities returned {StatusCode} after auth", capStatus); + } } - else + catch (Exception ex) { - _logger.LogWarning("⚠ Session capabilities returned {StatusCode} after auth", capStatus); + _logger.LogDebug(ex, "Failed to post session capabilities after auth"); } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to post session capabilities after auth"); - } - }); + }); + } } else {