fail quick connect closed on an unreachable valkey and restore the idempotent exchange
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
2026-09-26 14:28:45 +10:00
parent 6c54a02240
commit e1ca272d6c
17 changed files with 664 additions and 245 deletions
@@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.QuickConnect
if (result.Authenticated)
{
throw new InvalidOperationException("Request is already authorized");
throw new ConflictException("Request is already authorized");
}
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
@@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.QuickConnect
// The guard above is a read on shared state, so it cannot settle a race between instances; the claim can.
if (!await _store.TryClaimAuthorizationAsync(result.Secret, ExpiryOf(result)).ConfigureAwait(false))
{
throw new InvalidOperationException("Request is already authorized");
throw await RefusedClaimAsync(result.Secret).ConfigureAwait(false);
}
var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.QuickConnect
{
AssertActive();
var result = await _store.TryConsumeAuthorizationAsync(secret).ConfigureAwait(false);
var result = await _store.GetAuthorizationAsync(secret).ConfigureAwait(false);
if (result is null)
{
throw new ResourceNotFoundException("Unable to find request");
@@ -188,6 +188,20 @@ namespace Emby.Server.Implementations.QuickConnect
private static DateTime ExpiryOf(QuickConnectResult request) => request.DateAdded.AddMinutes(Timeout);
/// <summary>
/// Explains a refused claim. The claim outlives a failed mint on purpose, so it can mean either
/// that the request is authorized or that authorizing it did not finish; the two are told apart
/// by re-reading the request rather than reported as the same thing.
/// </summary>
private async Task<ConflictException> RefusedClaimAsync(string secret)
{
var current = await _store.GetRequestBySecretAsync(secret).ConfigureAwait(false);
return current?.Authenticated == true
? new ConflictException("Request is already authorized")
: new ConflictException("Request is being authorized elsewhere, or an earlier attempt to authorize it did not complete. Start quick connect again for a new code.");
}
private string GenerateSecureRandom(int length = 32)
{
Span<byte> bytes = stackalloc byte[length];