-
Notifications
You must be signed in to change notification settings - Fork 0
Error Handling
Every non-2xx response and every transport failure throws a CryptohopperError. Same shape across every SDK in every language.
public class CryptohopperError(
public val code: String, // "UNAUTHORIZED", "RATE_LIMITED", …
message: String,
public val status: Int, // HTTP status; 0 for transport failures
public val serverCode: Int? = null,
public val ipAddress: String? = null,
public val retryAfterMs: Long? = null,
) : RuntimeException(message)It extends RuntimeException, so callers catch it like any other Kotlin exception. There are no checked exceptions.
CryptohopperError.KNOWN_CODES // Set<String>
// "VALIDATION_ERROR", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND",
// "CONFLICT", "RATE_LIMITED", "SERVER_ERROR", "SERVICE_UNAVAILABLE",
// "DEVICE_UNAUTHORIZED", "NETWORK_ERROR", "TIMEOUT", "UNKNOWN"code |
HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR |
400 / 422 | Invalid parameters |
UNAUTHORIZED |
401 | Token missing or invalid |
DEVICE_UNAUTHORIZED |
402 | Mobile device not authorized (internal flow) |
FORBIDDEN |
403 | Missing scope or IP whitelist mismatch |
NOT_FOUND |
404 | Resource or endpoint not found |
CONFLICT |
409 | State conflict (e.g. duplicate create) |
RATE_LIMITED |
429 | Rate limit exceeded — SDK auto-retries by default |
SERVER_ERROR |
500 / 502 / 504 | Upstream error |
SERVICE_UNAVAILABLE |
503 | Server in maintenance |
NETWORK_ERROR |
— | Transport failure (DNS, connection refused) |
TIMEOUT |
— | Total request deadline exceeded |
UNKNOWN |
— | Anything else |
Server-side codes the SDK doesn't know about pass through verbatim on code. Compare with ==, never substring-match.
Three idiomatic ways to handle a typed error in Kotlin.
try {
ch.hoppers.list()
} catch (e: CryptohopperError) {
when (e.code) {
"NOT_FOUND" -> println("no such hopper")
"UNAUTHORIZED", "FORBIDDEN" ->
println("auth problem; IP we sent: ${e.ipAddress ?: "unknown"}")
"RATE_LIMITED" ->
println("rate limited; retry after ${e.retryAfterMs ?: 0}ms")
else -> throw e
}
}try {
ch.hoppers.get(999_999_999)
} catch (e: CryptohopperError) {
if (e.code == "NOT_FOUND") {
println("no such hopper")
} else {
throw e
}
}val result = runCatching { ch.user.get() }
.recoverCatching { e ->
if (e is CryptohopperError && e.code == "UNAUTHORIZED") {
// re-run OAuth, retry…
null
} else throw e
}Use runCatching when you want to thread the success/failure through a pipeline rather than wrap it in try.
If the server adds a new error code the SDK doesn't know about, it surfaces verbatim on code:
} catch (e: CryptohopperError) {
if (e.code !in CryptohopperError.KNOWN_CODES) {
// e.code is the literal server string, e.g. "MAINTENANCE_WINDOW"
log.warn("unknown server code: ${e.code}")
}
}code is a stable string and matches what the server sends — never substring-match. Use == or set membership.
The serverCode field is the numeric code integer from the JSON envelope. Cryptohopper uses this to identify rate-limit buckets and other server-side diagnostic codes. You usually don't need it — code (the string) is the right level of abstraction. It's exposed for advanced cases.
status == 0 means no response was received — the request never made it to the server, or the response wasn't HTTP. This happens for:
-
NETWORK_ERROR— DNS failure, connection refused, TLS handshake error, etc. -
TIMEOUT— total request deadline exceeded
For both, status == 0 and retryAfterMs == null. The exception's message field carries the underlying transport error.
The SDK configures OkHttp with connectTimeout, readTimeout, writeTimeout, and callTimeout all set to the configured timeout (default 30 seconds). callTimeout is the total deadline including retries — bytes dribbling slowly can't keep the connection open arbitrarily long, unlike a pure idle timeout.
If you bring your own OkHttpClient via Builder.httpClient(...), configure all four fields yourself. The default OkHttpClient.Builder() has no timeouts, which is rarely what you want.
Cancelling the surrounding coroutine scope cancels the in-flight request. The cancellation propagates as a kotlinx.coroutines.CancellationException — that's a special case in coroutines and should be re-thrown, not caught:
try {
ch.hoppers.list()
} catch (e: CryptohopperError) {
// handle SDK error
} catch (e: CancellationException) {
throw e // never swallow
}A bare catch (e: Exception) catches CancellationException too — avoid it for this reason.
-
Rate Limits — what
RATE_LIMITEDandretryAfterMsactually mean - Recipes#fail-fast-on-auth-errors-retry-on-transient-ones — copyable retry pattern
-
Authentication — debugging
UNAUTHORIZEDvsFORBIDDEN