.NET IHttpClientFactory + повторная попытка Microsoft Resilience: тот же HttpRequestMessage повторно отправляется, обрабC#

Место общения программистов C#
Ответить
Anonymous
 .NET IHttpClientFactory + повторная попытка Microsoft Resilience: тот же HttpRequestMessage повторно отправляется, обраб

Сообщение Anonymous »

У меня есть HttpClient, созданный с помощью IHttpClientFactory, и Microsoft.Extensions.Http.Resilience повторяет попытку 401. Конвейер:
  • Основной: HttpClientHandler (

    Код: Выделить всё

    AllowAutoRedirect = false
    )
  • Делегирование: .AddHttpMessageHandler()
  • Самый внешний: .AddResilienceHandler(...) с:

    Код: Выделить всё

    HttpRetryStrategyOptions { MaxRetryAttempts = 1, Delay = TimeSpan.Zero }
  • Код: Выделить всё

    ShouldHandle
    = 401
  • вызывает IAuthenticationTokenCache.ForceRefreshApiTokenAsync() (одноэлементный кеш)

Обработчик аутентификации отмечает заголовок каждой попытки:

Код: Выделить всё

protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct)
{
request.Headers.Remove("X-Auth-Handler-Stamp");
request.Headers.TryAddWithoutValidation("X-Auth-Handler-Stamp", Guid.NewGuid().ToString("N"));

var jwt = await _tokenCache.GetTokenForApi().ConfigureAwait(false);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);

return await base.SendAsync(request, ct).ConfigureAwait(false);
}
Регистрация (упрощенная):

Код: Выделить всё

services.AddHttpClient("my-http-client")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
AllowAutoRedirect = false
})
.AddHttpMessageHandler()
.AddResilienceHandler("RetryOnUnauthorized", (builder, sp) =>
{
var cache = sp.GetRequiredService(); // singleton
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 1,
Delay = TimeSpan.Zero,
ShouldHandle = new PredicateBuilder()
.HandleResult(r => r.StatusCode == HttpStatusCode.Unauthorized),
OnRetry = async _ =>  await cache.ForceRefreshApiTokenAsync().ConfigureAwait(false)
});
});
Ожидается:
При первой ошибке 401 обработчик устойчивости повторяет попытку, весь конвейер запускается снова и изменяется X-Auth-Handler-Stamp.
Факт:
Я вижу два HTTP-вызова, но идентичные заголовки (включая X-Auth-Handler-Stamp) отправляются оба раза — как если бы то же самое HttpRequestMessage было отправлено повторно, а обработчик аутентификации больше не запускался.
Примечания:
  • Используется клиент с правильным именем.
  • Кэш одноэлементный и обновляется правильно.
  • Нет перенаправлений, нет прокси-аутентификации, контент можно использовать повторно.
  • Добавление .Handle() не изменило поведение.
Вопрос:
Есть ли ситуация, когда повторная отправка происходит внутри HttpClientHandler (в обход обработчиков делегирования), или я неправильно понимаю поведение/порядок повторных попыток? Что мне следует сделать, чтобы гарантировать запуск обработчика аутентификации и получение нового токена?
Что я пробовал
  • Построил конвейер с помощью IHttpClientFactory:

    Код: Выделить всё

    HttpClientHandler (AllowAutoRedirect = false
    )
  • Код: Выделить всё

    .AddHttpMessageHandler()
  • Код: Выделить всё

    .AddResilienceHandler(...)
    added last with HttpRetryStrategyOptions (retry on 401, MaxRetryAttempts = 1, Delay = TimeSpan.Zero).
[*]Гарантированный срок службы:
  • Код: Выделить всё

    IAuthenticationTokenCache
    Singleton (используется как обработчиком, так и OnRetry).
  • Делегирование обработчиков Transient.
[*]Проверено: я использую именованный клиент, у которого есть этот конвейер.

[*]Поместил заголовок каждой попытки внутри обработчика аутентификации и перезаписал его при каждой отправке:

Код: Выделить всё

request.Headers.Remove("X-Auth-Handler-Stamp");
request.Headers.TryAddWithoutValidation("X-Auth-Handler-Stamp", Guid.NewGuid().ToString("N"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);

[*]Также помечен тот же HttpRequestMessage через request.Options для подсчета попыток:

Код: Выделить всё

private static readonly HttpRequestOptionsKey AttemptKey = new("Attempt");
request.Options.TryGetValue(AttemptKey, out var attempt);
request.Options.Set(AttemptKey, attempt + 1);

[*]Настройка теста WireMock:
  • 1-й вызов с старым токеном → возвращает 401 и развивает сценарий.
  • 2-й вызов с новым токеном → должен вернуть 200.
[*]Добавлен .Handle() в MustHandle на всякий случай.

[*]Содержимое подтвержденного запроса можно использовать повторно (нет потоков, недоступных для поиска).


Что я могу сделать? ожидается
  • В случае 401 повторная попытка обеспечения устойчивости должна повторно войти во весь конвейер, поэтому:

    Код: Выделить всё

    AuthenticationDelegatingHandler.SendAsync
    выполняется дважды (один раз за попытку).
  • Код: Выделить всё

    X-Auth-Handler-Stamp (GUID) changes between attempts.
    
    [*]AuthorizationЗаголовок 
    перезаписывается обновленным токеном при повторной попытке.
  • WireMock сопоставляет второй вызов с новым токеном и возвращает 200.

Что происходит на самом деле
  • Я вижу два HTTP-вызова, но:

    Код: Выделить всё

    X-Auth-Handler-Stamp
    идентичен в обоих случаях.
  • Заголовки на уровне проводов идентичны; похоже, что то же самое сообщение HttpRequestMessage было отправлено повторно.
  • Это говорит о том, что повторная отправка может происходить под обработчиками делегирования (внутри HttpClientHandler) или обработчик аутентификации не вызывается повторно при повторной попытке.

Любое минимальное воспроизведение/исправление приветствуется.

Подробнее здесь: https://stackoverflow.com/questions/797 ... message-re
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»