Skip to content

Exceptions RU

Алексей . edited this page Jul 26, 2026 · 1 revision

Исключения

English Русский

Любая доменная ошибка библиотеки наследуется от LogsParserException. Эта страница описывает иерархию, точное условие возникновения каждого типа, дополнительные члены RateLimitExceededException и правило повторных попыток, из-за которого базовый тип является несущей конструкцией, а не украшением.

Иерархия

Все десять типов находятся в одном плоском пространстве имён LogsParser.Exceptions независимо от папки, в которой лежит файл (Exceptions/Http/, Exceptions/Parsing/).

System.Exception
└── LogsParserException                      — корень доменной иерархии, не sealed
    ├── HtmlParsingException                 — sealed
    └── LogsParserHttpException              — база для всех транспортных ошибок и ошибок аутентификации, не sealed
        ├── AuthenticationRequiredException  — sealed
        ├── AuthenticationFailedException    — sealed
        ├── TwoFactorAuthenticationException — sealed
        ├── CsrfTokenNotFoundException       — sealed
        ├── AccountConfigurationException    — sealed
        ├── ReactShieldBypassException       — sealed
        └── RateLimitExceededException       — sealed

Не запечатаны только LogsParserException и LogsParserHttpException, поэтому наследоваться можно лишь от них — например, при реализации собственного ILogsParserDataSource. Сам LogsParserException библиотека никогда не выбрасывает: он существует, чтобы его перехватывали и от него наследовались.

Форма каждого исключения

Каждый тип помечен [Serializable] и объявляет три стандартных конструктора плюс [Obsolete]-конструктор десериализации. У LogsParserException и LogsParserHttpException конструктор десериализации объявлен как protected (от них наследуются), у восьми запечатанных типов — как private.

[Serializable]
public class LogsParserException : Exception
public LogsParserException()
public LogsParserException(string message)
public LogsParserException(string message, Exception innerException)
[Obsolete("Formatter-based serialization is obsolete.")]
protected LogsParserException(SerializationInfo info, StreamingContext context)

Единственное отклонение — RateLimitExceededException: конструктор без параметров он сохраняет, но два конструктора с сообщением заменены перегрузками, которые дополнительно несут данные о лимите запросов, см. RateLimitExceededException ниже.

Справочник

Все условия ниже взяты из реальных мест выброса в LogsParserHttpDataSource, во внутреннем LogsParserAuthenticator, во внутреннем ReactShieldBypass и в LogsHtmlParser.

Исключение Когда выбрасывается Типичная причина Что с этим делать
LogsParserException Библиотекой не выбрасывается. — Перехватывать как общий обработчик всего, что библиотека считает доменной ошибкой.
HtmlParsingException LogsHtmlParser.ParseAdminActivity не находит поля периода или tbody; LogsHtmlParser.ParseTopOperations не находит дату или tbody. На вход попала не та страница (страница ошибки, случайно переданная страница входа) либо изменилась вёрстка сайта. Убедиться, что HTML действительно является страницей отчёта. Если сайт изменился, нужно обновить регулярные выражения в Parsing/. Обратите внимание: ParseLogs не выбрасывает исключение — он деградирует до пустого результата.
LogsParserHttpException Ответ имеет неуспешный статус, не попадающий ни в один из обрабатываемых случаев (302 на /login, /authenticator, /profile или 429); также оборачивает последнее временное исключение после исчерпания MaxRetryAttempts. 4xx/5xx от сервиса, ошибка DNS/сокета/TLS, сбой прокси, таймаут. Для временного случая изучить InnerException. Увеличить MaxRetryAttempts либо починить сетевую доступность.
AuthenticationRequiredException Сервис ответил 302 на /login или на /authenticator, а credentials равен null. Источник данных создан без LogsParserCredentials, а сохранённые cookie уже недействительны. Создать LogsParserHttpDataSource с учётными данными либо восстановить хранилище cookie с действующей сессией.
AuthenticationFailedException GET /login возвращает неуспешный статус; POST /login перенаправляет обратно на /login; либо в ветку /login вошли, когда счётчик попыток аутентификации запроса (3 попытки, общий для веток /login и /authenticator) уже исчерпан. Неверные логин или пароль; страница входа недоступна; сессия не стабилизируется. Проверить LogsParserCredentials.Login и .Password. Не повторять запрос вслепую — транспорт уже сделал три попытки.
TwoFactorAuthenticationException GET /authenticator завершается неуспешно и без заголовка Location; POST /authenticator перенаправляет обратно на /authenticator; секрет TOTP пуст или содержит символы вне Base32; не разрешается ни America/Juneau, ни Alaskan Standard Time; либо в ветку /authenticator вошли, когда счётчик попыток аутентификации запроса (3 попытки, общий для веток /login и /authenticator) уже исчерпан. Неверный или некорректный TotpSecret, расхождение часов, хост без базы часовых поясов (урезанный контейнер, InvariantGlobalization). Проверить секрет Base32 и системное время. Установить базу tz на хосте — молчаливого отката на UTC здесь нет намеренно.
CsrfTokenNotFoundException В теле страницы /login или /authenticator отсутствует <meta name="csrf-token" content="...">. Ответ не является настоящей страницей формы: заглушка анти-DDoS, страница ошибки или внедрённый HttpClient, обработчик которого следует за перенаправлениями и «проглотил» 302. Убедиться, что внедрённый HttpClient использует AllowAutoRedirect = false. Иначе — разобрать, что именно вернул сервис.
AccountConfigurationException Сервис ответил 302 на /profile. Сам аккаунт logsparser не настроен на сайте (не выбран доступ к серверам, профиль не заполнен). Исправить аккаунт в веб-интерфейсе сайта. Повторные попытки не помогут.
ReactShieldBypassException Страница испытания продолжает выдаваться после 3 решённых токенов; из полезной нагрузки извлечено менее 3 hex-кандидатов; ни один кандидат не расшифровывается; либо при решении произошла непредвиденная ошибка (она попадает в InnerException). Анти-DDoS-щит изменил формат полезной нагрузки либо IP жёстко заблокирован, а не отправлен на испытание. Повторить позже с тем же хранилищем cookie. Устойчивый сбой означает, что решатель ReactShieldBypass нуждается в обновлении.
RateLimitExceededException HTTP 429 при WaitForRateLimitReset = false либо 429 без заголовка X-Ratelimit-Reset после исчерпания бюджета попыток. Слишком много запросов в пределах окна лимита. Прочитать RetryAfterSeconds / ResetAt и выдержать паузу либо выставить WaitForRateLimitReset = true и позволить транспорту подождать самому.

RateLimitExceededException

Единственное исключение с дополнительным состоянием.

[Serializable]
public sealed class RateLimitExceededException : LogsParserHttpException

Члены

Свойство Тип Описание
RetryAfterSeconds int Сколько секунд ждать до повтора. Берётся из заголовка ответа Retry-After, если он разбирается в положительное целое; иначе вычисляется как 2^(retryAttempt + 1), но не менее 1. Ноль, если использован конструктор без параметров.
ResetAt DateTimeOffset? Момент сброса окна, взятый из заголовка X-Ratelimit-Reset (Unix-секунды). null, если этот заголовок ни разу не встречался, а также в конструкторах, которые его не принимают.

Конструкторы

public RateLimitExceededException()
public RateLimitExceededException(string message, int retryAfterSeconds)
public RateLimitExceededException(string message, int retryAfterSeconds, DateTimeOffset? resetAt)
public RateLimitExceededException(string message, int retryAfterSeconds, Exception innerException)
Конструктор RetryAfterSeconds ResetAt
() 0 null
(string, int) аргумент null
(string, int, DateTimeOffset?) аргумент аргумент
(string, int, Exception) аргумент null

Перегрузок (string message) и (string message, Exception innerException) у этого типа нет. Транспорт всегда использует трёхаргументную форму (string, int, DateTimeOffset?), поэтому оба свойства заполнены, если заголовок присутствовал. Тип также переопределяет GetObjectData, сохраняя ResetAt в виде тиков со значением -1 в качестве признака null.

Когда оно возникает

При HTTP 429 транспорт делает одно из трёх:

  1. WaitForRateLimitReset = false — сразу выбрасывает RateLimitExceededException.
  2. WaitForRateLimitReset = true (по умолчанию) и известен X-Ratelimit-Reset — спит до момента сброса плюс 1 секунда и повторяет запрос, не расходуя попытку, поэтому исключение вообще не возникает.
  3. WaitForRateLimitReset = true, но момент сброса неизвестен — спит RetryAfterSeconds и расходует одну попытку; когда счётчик достигает MaxRetryAttempts (по умолчанию 5), выбрасывается исключение.
using LogsParser;
using LogsParser.Exceptions;
using LogsParser.Models;
using LogsParser.Net;

using var dataSource = new LogsParserHttpDataSource(
    credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"),
    options: new LogsParserHttpOptions { WaitForRateLimitReset = false });

var client = new LogsParserClient(dataSource);

try
{
    var page = await client.GetLogsAsync(new LogsQuery(ServerId: 201));
    Console.WriteLine($"{page.Entries.Count} entries");
}
catch (RateLimitExceededException exception)
{
    Console.WriteLine($"Rate limited: retry after {exception.RetryAfterSeconds}s, window resets at {exception.ResetAt}");
    await Task.Delay(TimeSpan.FromSeconds(exception.RetryAfterSeconds));
}

Почему важен базовый тип: повтор против проброса

LogsParserHttpDataSource.GetContentAsync оборачивает каждую попытку в try/catch с тремя ветками в таком порядке:

Перехвачено Поведение
LogsParserException (и всё, что от него наследуется) Пробрасывается немедленно. Счётчик попыток не увеличивается, задержка не применяется.
OperationCanceledException Пробрасывается без изменений.
Любое другое Exception Считается временным: счётчик попыток увеличивается, цикл спит 2^retryAttempt секунд и повторяет запрос. На последней попытке исключение оборачивается в LogsParserHttpException, а исходное попадает в InnerException.

Отсюда следуют два вывода.

  • Доменная ошибка не стоит времени. Неверные учётные данные, ненастроенный аккаунт или сломанный разбор завершаются на первой же попытке, а не после пяти повторов с экспоненциальной задержкой.
  • Любое новое исключение обязано наследоваться от LogsParserException. Доменная ошибка без такого наследования молча становится повторяемой: её перехватывает последняя ветка, повторяет запрос с задержкой и в итоге отдаёт вызывающему коду обёртку LogsParserHttpException с сообщением о сбое после N попыток. Это касается и исключений, которые вы выбрасываете из собственной реализации ILogsParserDataSource: наследуйтесь от LogsParserException или LogsParserHttpException, и контракт транспорта останется целым.

Обратите внимание, что ветки аутентификации и анти-DDoS-испытания вообще не попадают в эту классификацию: решение React-испытания и ожидание сброса лимита выполняют continue без расхода попытки, а ветки аутентификации ограничены собственным счётчиком из 3 попыток.

Исключения аргументов

Ошибки вызывающего кода выражаются стандартными исключениями BCL и намеренно не входят в доменную иерархию: они сигнализируют об ошибке в вызывающем коде, а не о сбое сервиса.

Исключение Где возникает Условие
ArgumentNullException конструктор LogsParserClient dataSource равен null.
ArgumentNullException LogsParserHttpDataSource.GetContentAsync request равен null.
ArgumentNullException LogsRequestUriBuilder.BuildLogsUri / BuildAdminActivityUri / BuildTopOperationsUri query равен null.
ArgumentNullException MemoryCookieStorage.SetCookies cookies равен null.
ArgumentNullException LogsParserLogging.UseLoggerFactory loggerFactory равен null.
ArgumentOutOfRangeException LogsRequestUriBuilder.BuildLogsUri ServerId <= 0, Limit <= 0 или Page <= 0.
ArgumentException LogsRequestUriBuilder.BuildLogsUri Sort не равен "desc" или "asc"; ключ из AdditionalParameters конфликтует с зарезервированным параметром запроса или не соответствует шаблону dynamic[n].
ArgumentException LogsHtmlParser.ParseLogs / ParseAdminActivity / ParseTopOperations, LogsFilterCatalogParser.Parse html пуст или состоит из пробельных символов (для null возникает ArgumentNullException).

Поскольку они не являются LogsParserException, общий обработчик транспорта считает их временными — для цикла повторов это верное поведение, но означает, что ошибка аргумента внутри собственного источника данных будет повторена несколько раз, прежде чем о ней сообщат.

Обработка исключений

Перехватывайте сначала конкретные типы, на которые вы действительно можете отреагировать, а затем LogsParserException как общий обработчик.

using LogsParser;
using LogsParser.Exceptions;
using LogsParser.Models;
using LogsParser.Net;

using var dataSource = new LogsParserHttpDataSource(
    credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"));

var client = new LogsParserClient(dataSource);

try
{
    var page = await client.GetLogsAsync(new LogsQuery(ServerId: 201), cancellationToken: cancellationToken);
    Console.WriteLine($"{page.Entries.Count} entries");
}
catch (AuthenticationRequiredException)
{
    Console.WriteLine("No credentials configured and the session has expired.");
}
catch (AuthenticationFailedException exception)
{
    Console.WriteLine($"Login rejected: {exception.Message}");
}
catch (TwoFactorAuthenticationException exception)
{
    Console.WriteLine($"Two-factor confirmation failed: {exception.Message}");
}
catch (AccountConfigurationException)
{
    Console.WriteLine("The account is not configured on the site — fix it in the web UI.");
}
catch (RateLimitExceededException exception)
{
    Console.WriteLine($"Rate limited, retry after {exception.RetryAfterSeconds}s.");
}
catch (HtmlParsingException exception)
{
    Console.WriteLine($"The page layout changed: {exception.Message}");
}
catch (LogsParserException exception)
{
    // CsrfTokenNotFoundException, ReactShieldBypassException, LogsParserHttpException
    // and anything added later all land here.
    Console.WriteLine($"LogsParser failed: {exception.Message}");
}

Порядок важен: LogsParserHttpException является базой для семи запечатанных типов, поэтому catch по нему должен идти после них, а LogsParserException — последним из всех.

Отмена

OperationCanceledException не входит в иерархию и пробрасывается без изменений при срабатывании CancellationToken вызывающей стороны: он не повторяется и не оборачивается, и то же верно для TaskCanceledException, возникающего в задержках повтора и ожидания лимита. Цикл повторов также завершается OperationCanceledException, если токен уже отменён к началу очередной итерации. Перехватывайте его отдельно и вне доменных обработчиков:

try
{
    var page = await client.GetLogsAsync(new LogsQuery(ServerId: 201), cancellationToken: cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
    Console.WriteLine("Cancelled by the caller.");
}
catch (LogsParserException exception)
{
    Console.WriteLine($"LogsParser failed: {exception.Message}");
}

См. также

Clone this wiki locally