Skip to content

Errors & retries

Every public service method throws a typed exception for the error conditions you care about. Network blips are auto-retried via Polly before surfacing.

Exception When Recovery
STXWrongCredentialsException Email or password rejected at login. Legacy path only Prompt for fresh credentials
STXSessionExpiredException JWT and refresh token both expired. Legacy path only Re-authenticate
STXTokenExpiredException JWT expired but refresh token is still valid Call STXTokenService.RefreshTokenAsync, or rely on keepSessionAlive: true
STXCancelOnDisconnectNotEnabledException ConfirmOrderAsync(cancelOnDisconnect: true) called before joining STXActiveOrdersChannel Join the channel first, or pass cancelOnDisconnect: false
STXGeoComplyException Geo-compliance check failed User is outside a permitted jurisdiction; surface to your UI
GraphQLHttpRequestException Non-200 HTTP response from the GraphQL endpoint Usually transient; retries handled internally (see below)

All STX-prefixed exceptions live in STX.Sdk.Exceptions.

try
{
var me = await identity.GetMeAsync();
}
catch (STXRequestFailedException ex)
{
// A rejected signature lands here. The usual cause is clock skew: the server
// allows 30 seconds, so check NTP before suspecting the key.
_logger.LogError(ex, "Could not authenticate");
}
catch (STXGeoComplyException ex)
{
// User-facing: "Trading isn't available in your region."
_logger.LogWarning(ex, "Geo block");
}

Example: refresh on expired token (legacy path)

Section titled “Example: refresh on expired token (legacy path)”

Only relevant to email and password. API keys have no token to expire, so none of this is needed.

When not using keepSessionAlive: true, wrap trading calls with a token-refresh retry:

async Task<T> WithSession<T>(Func<Task<T>> call)
{
try { return await call(); }
catch (STXTokenExpiredException)
{
await _tokens.RefreshTokenAsync();
return await call();
}
catch (STXSessionExpiredException)
{
await _login.LoginAsync(_email, _password);
return await call();
}
}
var order = await WithSession(() =>
_orders.ConfirmOrderAsync(price, qty, marketId, action, type));

Retry is applied per call site, not by the transport, so it covers some methods and not others. As of 1.6.0 it covers every order mutation, all of STXMarketService, STXIdentityService, terms-and-conditions acceptance, and login and token refresh, which previously had no retry at all.

These still call the GraphQL client directly and are not retried:

Not retried
STXOrderService.GetMyOrdersAsync
STXTradeService.GetMyTradesAsync, GetMyTradesForOrderAsync
STXSettlementService.GetMySettlementsAsync
STXEventService.GetEventInfosAsync
STXTermsAndConditionsService.GetTermsAndConditionsAsync
STXGeoFencingLicenseService

If a brief outage during one of those matters to you, wrap it yourself. Everywhere else, don’t add your own retry on top: double-retry amplifies an outage rather than absorbing it.

Where it does apply the policy is RequestNumberOfRetry = 3, meaning 3 retries after the initial call, so 4 requests in total, with exponential backoff plus jitter: roughly 200ms, 400ms and 800ms, so about 1.4s of sleep on top of the per-attempt HTTP timeout. Size any outer deadline or circuit breaker against that, not against a single request.

The policy retries anything that could plausibly succeed on a second attempt:

Retried Not retried
HttpRequestException and other network failures STXWrongCredentialsException
5xx from the GraphQL endpoint STXUnauthorizedException, STXSessionExpiredException, STXTokenExpiredException
404, which is what an ingress returns mid-deployment STXBadQueryObjectException, STXBadArgumentException, ArgumentException
Timeouts (TaskCanceledException wrapping TimeoutException) 429, and genuine cancellation

Credential errors are excluded deliberately, including a bare HTTP 401 or 403. Repeating a rejected credential cannot succeed, and on the legacy background re-login path it actively causes harm by driving the account towards a lockout.

If a failure persists past the built-in retries, STXRequestFailedException surfaces with the original exception as its InnerException, so the underlying cause (an HTTP status, a DNS failure, a timeout) is still available:

catch (STXRequestFailedException ex) when (ex.InnerException is GraphQLHttpRequestException http)
{
_logger.LogWarning("GraphQL returned {Status}", http.StatusCode);
}

The exchange rate-limits by IP + account. Bursty order placement can return an HTTP 429. The SDK does not retry 429s (retrying is what the limiter is pushing back against). If you see them, lower your request rate or batch via ConfirmOrdersAsync.

Enable Debug logging to see full GraphQL request/response bodies:

services.AddLogging(builder =>
builder.SetMinimumLevel(LogLevel.Debug).AddConsole());

Every call is logged with its operation name, variables, and elapsed time. That’s usually enough to tell whether an error was transient or a schema mismatch.

v1.5.9Changelogllms.txtllms-full.txt