Skip to content

Exceptions

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

Exceptions

English Русский

Every domain error the library raises derives from LogsParserException. This page lists the hierarchy, the exact condition behind each type, the extra members on RateLimitExceededException, and the retry rule that makes the base type load-bearing rather than decorative.

Hierarchy

All ten types live in the single flat namespace LogsParser.Exceptions, regardless of the folder they sit in (Exceptions/Http/, Exceptions/Parsing/).

System.Exception
└── LogsParserException                      — root of the domain hierarchy, not sealed
    ├── HtmlParsingException                 — sealed
    └── LogsParserHttpException              — base for every transport/auth failure, not sealed
        ├── AuthenticationRequiredException  — sealed
        ├── AuthenticationFailedException    — sealed
        ├── TwoFactorAuthenticationException — sealed
        ├── CsrfTokenNotFoundException       — sealed
        ├── AccountConfigurationException    — sealed
        ├── ReactShieldBypassException       — sealed
        └── RateLimitExceededException       — sealed

Only LogsParserException and LogsParserHttpException are non-sealed, so they are the only two types you can derive from — for example when implementing a custom ILogsParserDataSource. LogsParserException itself is never thrown by the library; it exists to be caught and to be inherited.

Shape of every exception

Each type is [Serializable] and declares the three standard constructors plus an [Obsolete] serialization constructor. LogsParserException and LogsParserHttpException declare the serialization constructor as protected (they are inheritable); the eight sealed types declare it as 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 is the one deviation: it keeps the parameterless constructor but replaces the two message constructors with overloads that also carry the rate-limit data — see RateLimitExceededException below.

Reference

Every trigger below is the actual condition at the throw site in LogsParserHttpDataSource, the internal LogsParserAuthenticator, the internal ReactShieldBypass, or LogsHtmlParser.

Exception Thrown when Typical cause What to do about it
LogsParserException Never thrown by the library. — Catch it as the catch-all for anything this library considers a domain error.
HtmlParsingException LogsHtmlParser.ParseAdminActivity cannot find the period inputs or the tbody; LogsHtmlParser.ParseTopOperations cannot find the date or the tbody. The HTML is not the expected page (an error page, a login page passed in by mistake), or the site's markup changed. Verify the HTML really is the report page. If the site changed, the regexes in Parsing/ need updating. Note that ParseLogs does not throw — it degrades to an empty result.
LogsParserHttpException A response has a status code that is not success and is not one of the handled cases (302 to /login, /authenticator, /profile, or 429); also wraps the last transient exception once MaxRetryAttempts is exhausted. 4xx/5xx from the service, DNS/socket/TLS failure, proxy failure, timeout. Inspect InnerException for the transient case. Raise MaxRetryAttempts, or fix connectivity.
AuthenticationRequiredException The service answered 302 to /login or to /authenticator while credentials is null. The data source was constructed without LogsParserCredentials, and the stored cookies are no longer valid. Construct LogsParserHttpDataSource with credentials, or restore a cookie storage that still holds a valid session.
AuthenticationFailedException GET /login returns a non-success status; POST /login redirects back to /login; or the /login branch was entered after the request's auth counter (3 attempts, shared by the /login and /authenticator branches) was already used up. Wrong login or password; the login page is unavailable; the session never stabilises. Check LogsParserCredentials.Login / .Password. Do not retry blindly — the transport already tried three times.
TwoFactorAuthenticationException GET /authenticator fails without a Location header; POST /authenticator redirects back to /authenticator; the TOTP secret is empty or contains non-Base32 symbols; neither America/Juneau nor Alaskan Standard Time resolves; or the /authenticator branch was entered after the request's auth counter (3 attempts, shared by the /login and /authenticator branches) was already used up. A wrong or malformed TotpSecret, clock skew, or a host with no time-zone database (a trimmed container, InvariantGlobalization). Verify the Base32 secret and the system clock. Install the tz database on the host — there is deliberately no silent UTC fallback.
CsrfTokenNotFoundException <meta name="csrf-token" content="..."> is absent from the /login or /authenticator page body. The response is not the real form page: an anti-DDoS interstitial, an error page, or an injected HttpClient whose handler follows redirects and swallowed the 302. Ensure the injected HttpClient uses AllowAutoRedirect = false. Otherwise inspect what the service actually returned.
AccountConfigurationException The service answered 302 to /profile. The logsparser account itself is not configured on the site (no server access selected, profile incomplete). Fix the account in the site's UI. No amount of retrying helps.
ReactShieldBypassException The challenge page keeps being served after 3 solved tokens; the challenge payload yields fewer than 3 hex candidates; no candidate decrypts; or any unexpected error occurs while solving (wrapped as InnerException). The anti-DDoS shield changed its payload format, or the IP is being hard-blocked rather than challenged. Retry later from the same cookie storage. A persistent failure means the ReactShieldBypass solver needs updating.
RateLimitExceededException HTTP 429 with WaitForRateLimitReset = false, or 429 without an X-Ratelimit-Reset header once the retry budget is exhausted. Too many requests in the rate-limit window. Read RetryAfterSeconds / ResetAt and back off, or set WaitForRateLimitReset = true and let the transport wait.

RateLimitExceededException

The only exception carrying extra state.

[Serializable]
public sealed class RateLimitExceededException : LogsParserHttpException

Members

Property Type Description
RetryAfterSeconds int Seconds to wait before retrying. Taken from the Retry-After response header when it parses to a positive integer; otherwise computed as 2^(retryAttempt + 1), at least 1. Zero when the parameterless constructor was used.
ResetAt DateTimeOffset? The moment the window resets, taken from the X-Ratelimit-Reset header (Unix seconds). null when that header was never seen, and on the constructors that do not accept it.

Constructors

public RateLimitExceededException()
public RateLimitExceededException(string message, int retryAfterSeconds)
public RateLimitExceededException(string message, int retryAfterSeconds, DateTimeOffset? resetAt)
public RateLimitExceededException(string message, int retryAfterSeconds, Exception innerException)
Constructor RetryAfterSeconds ResetAt
() 0 null
(string, int) the argument null
(string, int, DateTimeOffset?) the argument the argument
(string, int, Exception) the argument null

There is no (string message) and no (string message, Exception innerException) overload on this type. The transport always uses the three-argument (string, int, DateTimeOffset?) form, so both properties are populated whenever the header was present. The type also overrides GetObjectData, storing ResetAt as ticks with -1 as the null sentinel.

When it is thrown

On HTTP 429 the transport does one of three things:

  1. WaitForRateLimitReset = false — throws RateLimitExceededException immediately.
  2. WaitForRateLimitReset = true (the default) and X-Ratelimit-Reset is known — sleeps until reset + 1 second and retries without consuming a retry attempt, so it never surfaces as an exception.
  3. WaitForRateLimitReset = true but no reset timestamp is known — sleeps RetryAfterSeconds and consumes one retry attempt; when the attempt counter reaches MaxRetryAttempts (default 5) it throws.
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));
}

Why the base type matters: retried vs. rethrown

LogsParserHttpDataSource.GetContentAsync wraps every attempt in a try/catch with three branches, in this order:

Caught Behaviour
LogsParserException (and everything deriving from it) Rethrown immediately. The retry attempt counter is not incremented and no delay is applied.
OperationCanceledException Rethrown unchanged.
Any other Exception Treated as transient: the attempt counter increments, the loop sleeps 2^retryAttempt seconds and retries. On the last attempt it is wrapped in LogsParserHttpException and thrown with the original as InnerException.

Two consequences follow.

  • A domain error never costs latency. Wrong credentials, an unconfigured account or a broken parse fail on the first attempt instead of being retried five times with exponential backoff.
  • Any new exception must derive from LogsParserException. A domain error that does not becomes silently retryable: it is caught by the last branch, retried with backoff, and finally reaches the caller re-wrapped as a LogsParserHttpException whose message says the request failed after N attempts. This applies to exceptions you throw from a custom ILogsParserDataSource implementation too — derive from LogsParserException or LogsParserHttpException and the transport's contract stays intact.

Note that the auth and challenge branches sit outside this classification entirely: solving the React challenge and waiting out a rate-limit reset continue the loop without consuming a retry attempt, and the auth branches are bounded by their own counter of 3 attempts.

Argument exceptions

Caller mistakes surface as the BCL argument exceptions and are deliberately not part of the domain hierarchy — they signal a bug in the calling code, not a failure of the service.

Exception Raised by Condition
ArgumentNullException LogsParserClient constructor dataSource is null.
ArgumentNullException LogsParserHttpDataSource.GetContentAsync request is null.
ArgumentNullException LogsRequestUriBuilder.BuildLogsUri / BuildAdminActivityUri / BuildTopOperationsUri query is null.
ArgumentNullException MemoryCookieStorage.SetCookies cookies is null.
ArgumentNullException LogsParserLogging.UseLoggerFactory loggerFactory is null.
ArgumentOutOfRangeException LogsRequestUriBuilder.BuildLogsUri ServerId <= 0, Limit <= 0, or Page <= 0.
ArgumentException LogsRequestUriBuilder.BuildLogsUri Sort is not "desc" or "asc"; an AdditionalParameters key collides with a reserved query parameter or does not match dynamic[n].
ArgumentException LogsHtmlParser.ParseLogs / ParseAdminActivity / ParseTopOperations, LogsFilterCatalogParser.Parse html is empty or whitespace (null raises ArgumentNullException).

Because they are not LogsParserException, the transport's catch-all treats them as transient — which is the correct behaviour for the retry loop, but means an argument bug inside a custom data source will be retried before it is reported.

Handling exceptions

Catch the specific types you can actually act on, then LogsParserException as the catch-all.

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}");
}

Order matters: LogsParserHttpException is the base of seven of the sealed types, so a catch on it must come after any of them; LogsParserException must come last of all.

Cancellation

OperationCanceledException is not part of the hierarchy and is rethrown unchanged when the caller's CancellationToken fires — it is neither retried nor wrapped, and the same holds for the TaskCanceledException raised by the retry and rate-limit delays. The retry loop also exits with OperationCanceledException if the token is already cancelled when an iteration starts. Catch it separately, and outside the domain catches:

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}");
}

See also

Clone this wiki locally