Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Если API слишком медленный, приложение .NET получает 1 из 2 исключений в зависимости от того, какой тайм-аут сработал.
HttpClient.Timeout вызывает TaskCanceledException. Стандартный обработчик устойчивости из TimeoutRejectedException вызывает исключение Microsoft.Extensions.Http.Resilience из Polly. Они приходят из разных мест, имеют разные значения по умолчанию и нуждаются в отдельных catch блоках.
Какой тайм-аут сработал
| Timeout | Default | Что получает ваш код | Где вы это настроили |
|---|---|---|---|
HttpClient.Timeout |
100 секунд |
TaskCanceledException, с TimeoutException как его InnerException (.NET 5 и более поздние версии) |
HttpClient.Timeout |
| Время ожидания попытки стандартного обработчика | 10 секунд на попытку | Сначала ничего не происходит: обработчик повторяет попытку | AddStandardResilienceHandler(options => ...) |
| Общее время ожидания стандартного обработчика | 30 секунд, включая все повторные попытки | Polly.Timeout.TimeoutRejectedException |
AddStandardResilienceHandler(options => ...) |
HttpClient.Timeout
HttpClient.Timeout применяется к каждому запросу, который отправляет экземпляр HttpClient. Чтобы использовать другое время ожидания для одного запроса, передайте его CancellationToken из CancellationTokenSource с собственным временем ожидания. Применяется более короткий из двух вариантов. Установите Timeout.InfiniteTimeSpan, чтобы отключить это.
В .NET 5 и более поздних версиях тайм-аут вызывает TaskCanceledException, содержащий TimeoutException внутри. В более ранних версиях .NET Core внутреннее исключение отсутствует. В .NET Framework вы получаете HttpRequestException вместо него. Дополнительные сведения см. в разделе HttpClient.Timeout и Make HTTP-запросы с классом HttpClient.
Также TaskCanceledException означает, что пользователь отменил запрос, например пользователь, который закрыл страницу. Чтобы отличить тайм-аут от отмены, проверьте ex.InnerException is TimeoutException или проверьте, отменен ли ваш собственный токен.
Стандартный обработчик отказоустойчивости
AddStandardResilienceHandler() объединяет ограничитель частоты запросов, общий тайм-аут, повторные попытки, автоматический выключатель и тайм-аут попытки. Если одна попытка занимает более 10 секунд, тайм-аут попытки отменяет её, и стратегия повторных попыток снова пытается: до 3 повторных попыток, с экспоненциальной задержкой и jitter, начиная с 2 секунд. Когда весь запрос, включая повторные попытки, занимает более 30 секунд, общее время ожидания отменяет его, а ваш код получает TimeoutRejectedException.
TimeoutRejectedException является производным от Exception. Это ни TimeoutException, ни HttpRequestException, поэтому блок catch (HttpRequestException) не перехватывает это. Полный список значений по умолчанию см. в разделе Стандартные значения по умолчанию для обработчика устойчивости.
Например, если приложение .NET 10, использующее стандартный обработчик по умолчанию, вызывает API, на каждый ответ которого уходит от 11 до 15 секунд, приложение получает TimeoutRejectedException через 30 секунд, а его блок catch (HttpRequestException) не запускается.
Как обрабатывать тайм-ауты HttpClient
- Перехватите оба исключения при вызове API. Если вы используете стандартный обработчик, перехватите
TimeoutRejectedException. ПерехватитеTaskCanceledExceptionдляHttpClient.Timeout. - Отличайте тайм-аут от отмены. Считать
TaskCanceledExceptionтайм-аутом только если его внутреннее исключение —TimeoutException. Когда вызывающая сторона отменяет, остановитесь без лишнего шума. - Выберите тайм-ауты, подходящие для API. Если ответ API часто занимает более 10 секунд, измените тайм-ауты попытки и общий тайм-аут в
AddStandardResilienceHandler(options => ...). - Не повторяйте
POSTилиPATCH, если API делает это безопасным. Стандартный обработчик повторно выполняет все методы по умолчанию, включаяPOST. Вызовитеoptions.Retry.DisableForUnsafeHttpMethods(), чтобы исключитьPOST,PATCH,DELETE,PUTиCONNECT, илиoptions.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch), чтобы продолжать повторные попытки идемпотентныхPUTиDELETE. - Сообщите пользователю, что произошло. Отображайте «служба работает медленно, повторите попытку» вместо универсальной ошибки.
using Polly.Timeout;
public async Task<string?> GetForecastAsync(HttpClient client, CancellationToken cancellationToken)
{
try
{
return await client.GetStringAsync("https://api.contoso.com/forecast", cancellationToken);
}
catch (TimeoutRejectedException)
{
// Standard resilience handler: total timeout expired after all retries
return null;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// HttpClient.Timeout expired
return null;
}
catch (HttpRequestException)
{
// Network error, or an error status code after all retries
return null;
}
}
Как проверить, что ваше приложение обрабатывает тайм-ауты
Во время разработки тайм-ауты случаются редко, поэтому блоки catch редко выполняются. Способ тестирования определяет, находите ли вы ошибки раньше, чем ваши пользователи.
| Approach | Что вы найдете | То, что вы пропустили |
|---|---|---|
| Ожидание продакшна | Тайм-ауты реального времени | Все, пока пользователь не нажмет на него |
| Мокируйте API в тестах или позвольте ИИ-агенту для программирования написать мок | Выполняется ли блок catch, если мок выдает правильное исключение |
Реальная HttpClient настройка, повторные попытки обработчика устойчивости и исключение, которое он фактически вызывает. Приложению также требуется переключатель только для тестирования, чтобы получить доступ к mock-сервису. |
| Вызовите реальный API и надейтесь, что он работает медленно | Реальное поведение | Вы не можете сделать API медленным по требованию |
| Перехватывайте реальный трафик приложения и задерживайте ответы | Ваш настоящий HttpClient, обработчик отказоустойчивости и исключения |
Ничего в приложении не изменяется, поэтому ваш код не тестируется изолированно. Оставьте модульные тесты для этого. |
Попробуйте в вашем приложении
Dev Proxy перехватывает запросы приложения к API и задерживает ответы с помощью LatencyPlugin. .NET использует системный прокси-сервер, поэтому вам не нужно изменять код. Этот пример задерживает каждый ответ на 11–15 секунд, что дольше стандартного 10‑секундного тайм-аута попытки обработчика. Замените https://api.contoso.com на URL-адрес API, который вызывает ваше приложение.
Файл: devproxyrc.json
{
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/rc.schema.json",
"plugins": [
{
"name": "LatencyPlugin",
"enabled": true,
"pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
"configSection": "slowApi"
}
],
"urlsToWatch": [
"https://api.contoso.com/*"
],
"slowApi": {
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/latencyplugin.schema.json",
"minMs": 11000,
"maxMs": 15000
}
}
Запустите Dev Proxy devproxy --config-file devproxyrc.json и запустите приложение. При каждой попытке происходит тайм-аут, обработчик повторяет попытку, а через 30 секунд ваш код получает значение TimeoutRejectedException. Чтобы протестировать HttpClient.Timeout вместо этого, задайте для minMs значение больше, чем настроенный вами тайм-аут. Дополнительные сведения о настройке см. в разделе "Использование Dev Proxy с приложениями .NET". Чтобы установить Dev Proxy, см. статью "Настройка Dev Proxy".