Error Model
Everything the SDK throws is a MeroError. It’s an enum, so you switch over
the failure modes you care about.
MeroError
Section titled “MeroError”public enum MeroError: Error, Sendable { case http(HTTPError) // non-2xx HTTP response case authRevoked(reason: String, http: HTTPError) // token_reuse / token_revoked — terminal case rpc(RpcError) // JSON-RPC error from a contract case network(String) // transport/connectivity failure case emptyResponse(String) // expected a body, got none case authenticationFailed(String) case noCredentials // authenticate() with nothing to use case noRefreshToken case decoding(String) // response didn't match the expected type}MeroError also conforms to LocalizedError, so error.localizedDescription
gives a human-readable string.
HTTPError
Section titled “HTTPError”public struct HTTPError: Error, Sendable, Equatable { public let status: Int public let statusText: String public let url: String public let headers: [String: String] public let bodyText: String? // raw body if available (capped ~64 KiB) public var message: String { "HTTP \(status) \(statusText)" }}RpcError
Section titled “RpcError”public struct RpcError: Error, Sendable, Equatable { public let code: Int public let message: String public let type: String? public let data: JSONValue?}Handling pattern
Section titled “Handling pattern”do { let post: Post = try await mero.rpc.execute( contextId: "…", method: "get_post", argsJson: ["id": "42"])} catch let error as MeroError { switch error { case .rpc(let rpc): // Contract-level error print("contract error \(rpc.code): \(rpc.message)", rpc.data as Any) case .authRevoked: // Refresh token reused/revoked — session is over promptSignIn() case .http(let http): if http.status == 403 { print("permission denied") } else { print(http.message) } case .network(let detail): print("network error: \(detail)") default: print(error.localizedDescription) }}How 401s are handled
Section titled “How 401s are handled”The transport handles 401 internally: it runs a single token refresh and
retries the original request (see
the token lifecycle). You only
see an error if the refresh itself fails:
- Access token expired but refresh works → transparent; no error surfaces.
- Refresh fails (expired refresh token) → surfaces as
MeroError.http/.authenticationFailed; callawait mero.clearToken()and re-authenticate. x-auth-error: token_reuse | token_revoked→ never retried; surfaces asMeroError.authRevoked(reason:http:)and the bundle is cleared for you.
Retries & timeouts
Section titled “Retries & timeouts”Each request has a per-request timeout of MeroConfig.timeout (default 10s).
The retry engine is internal (not caller-invokable), but it’s worth knowing what
it does: up to 3 attempts with exponential backoff (base 250 ms, ±20%
jitter). It retries transient failures only — network errors, HTTP 5xx/429,
and URLError timeouts / connection loss / DNS failures. It never retries 4xx
(including 401/403) or MeroError.authRevoked. A 401 token_expired is
handled separately by the refresh path below, not by this retry loop.
Transport
Section titled “Transport”The retry engine above lives inside the transport and isn’t part of the public API. The public transport surface:
public protocol HttpClient: Sendable { func send<T: Decodable>(_ req: HttpRequest) async throws -> T func sendVoid(_ req: HttpRequest) async throws func sendRaw(_ req: HttpRequest) async throws -> (Data, HeadResult) func head(_ path: String, headers: [String: String]) async throws -> HeadResult func download(_ req: HttpRequest, to fileURL: URL) async throws -> HeadResult}URLSessionHttpClient is the built-in implementation; reach it as mero.http.
Its TransportHooks (getAuthToken, refreshToken, onTokenRefresh,
onAuthRevoked) are what Mero wires into its own token state — you don’t set
these yourself when using Mero.