SDK Error Handling
SDK Error Handling
Note - As of 2026-04-30, the per-service
@heossihq/qnsi-vault-sdkpackage is consolidated into the unified@heossihq/qnsiSDK (one package per language). New integrations should use:import { QnsiClient } from "@heossihq/qnsi"; const qnsi = new QnsiClient({ apiKey: process.env.QNSI_API_KEY! }); await qnsi.vault./* method */(...);See SDK overview for the consolidated package. The per-service shapes documented below remain accurate at the wire level (REST/gRPC) and are kept for reference.
SDK Error Handling
Every QNSI SDK distinguishes four kinds of failure so callers can branch on the failure mode without parsing error strings:
| Kind | When it fires |
|---|---|
| Network | DNS, TLS, timeout, or connection failure reaching the QNSI edge gateway |
| Auth | API key rejected at activation (HTTP 401/403 from /billing/v1/sdk/activate) |
| API | A QNSI service returned a 4xx/5xx with a structured body |
| Webhook | Signature mismatch, timestamp out of skew, malformed payload, missing fields |
The class names differ per language but the taxonomy is identical, so the same try/catch/Result shape ports across stacks.
TypeScript / Node.js
import { QnsiApiError, QnsiClient, QnsiNetworkError } from "@heossihq/qnsi";
const qnsi = new QnsiClient({ apiKey: process.env.QNSI_API_KEY! });
try {
await qnsi.vault.getSecret("missing");
} catch (err) {
if (err instanceof QnsiApiError) console.log("HTTP", err.statusCode, err.code);
else if (err instanceof QnsiNetworkError) console.log("could not reach QNSI:", err.message);
else throw err;
}
The unified package exports its typed error classes from @heossihq/qnsi.
Python
from qnsi import QnsiApiError, QnsiNetworkError, QnsiAuthError, QnsiError
with QnsiClient(api_key=os.environ["QNSI_API_KEY"]) as q:
try:
q.vault.get_secret("missing")
except QnsiApiError as exc:
print("HTTP", exc.status_code, exc.code, exc.body)
except QnsiNetworkError as exc:
print("could not reach QNSI:", exc)
except QnsiAuthError as exc:
print("api key rejected:", exc.code, exc.message)
All errors descend from QnsiError. See sdks/python/qnsi/src/qnsi/_errors.py.
Go
import (
"errors"
"github.com/heossihq/qnsi-public/sdks/go/qnsi"
)
if _, err := c.Vault().GetSecret(ctx, "missing"); err != nil {
var apiErr *qnsp.APIError
var netErr *qnsp.NetworkError
var authErr *qnsp.AuthError
switch {
case errors.As(err, &apiErr):
fmt.Println("HTTP", apiErr.StatusCode, apiErr.Code)
case errors.As(err, &netErr):
fmt.Println("could not reach QNSI:", netErr.Err)
case errors.As(err, &authErr):
fmt.Println("api key rejected:", authErr.Code)
}
}
All Go errors implement the unexported qnsiError() marker so qnsi.Error works as a type-narrowing predicate. See sdks/go/qnsi/internal/qnsicore/errors.go.
Rust
match c.vault().get_secret("missing").await {
Err(qnsi::Error::Api(e)) if e.status_code == 404 => println!("not found"),
Err(qnsi::Error::Network(e)) => println!("could not reach QNSI: {e}"),
Err(qnsi::Error::Auth(e)) => println!("api key rejected: {e:?}"),
Err(qnsi::Error::Webhook(e)) => println!("webhook: {e}"),
Err(e) => return Err(e),
Ok(secret) => println!("{secret:?}"),
}
All errors flow through the qnsi::Error enum. See sdks/rust/qnsi/src/errors.rs.
JVM / Android
import io.heossi.qnsi.QnsiApiException
import io.heossi.qnsi.QnsiAuthException
import io.heossi.qnsi.QnsiNetworkException
import io.heossi.qnsi.QnsiWebhookException
try {
val secret = qnsi.vault.getSecret("missing")
} catch (e: QnsiApiException) {
if (e.statusCode == 404) println("not found") else println("api error ${e.statusCode} ${e.code}")
} catch (e: QnsiNetworkException) {
println("could not reach QNSI: ${e.message}")
} catch (e: QnsiAuthException) {
println("api key rejected: ${e.code}")
} catch (e: QnsiWebhookException) {
println("webhook: ${e.message}")
}
All SDK errors extend the unchecked QnsiException base class - catch QnsiException to handle any failure uniformly. QnsiApiException exposes statusCode, the stable code string, and the raw body. See sdks/jvm/src/main/kotlin/com/heossi/qnsi/QnsiErrors.kt.
Status-code mapping
QNSI services map to standard HTTP semantics:
| Status | Meaning | Common cause |
|---|---|---|
| 400 | Bad request | Validation failure on request body |
| 401 | Unauthorised | API key invalid OR activation token expired (SDK retries once) |
| 402 | Payment required | Tier does not entitle the call (e.g. SSE on free tier) |
| 403 | Forbidden | RBAC / capability check failed |
| 404 | Not found | Resource id does not exist for the tenant |
| 409 | Conflict | Idempotency key reuse with mismatched body, version mismatch on update |
| 422 | Unprocessable | Semantically invalid request (e.g. unsupported algorithm name) |
| 429 | Too many requests | Quota exhausted; retry after Retry-After header |
| 502 | Bad gateway | Upstream service temporarily unavailable |
| 503 | Service unavailable | Tenant entitlement state cannot be resolved |
Each SDK surfaces the structured body of an API error so you can act on code (a stable string identifier) rather than parsing the human-readable message.
Webhook errors
Webhook verification helpers (parse_qnsi_webhook in Python, qnsi.ParseWebhook in Go, qnsi::parse_webhook in Rust, QnsiWebhooks.parse on JVM, per-service equivalents in TypeScript) return a typed error whose .reason field describes which check failed:
signature header must start with 'sha256='signature mismatch- HMAC verification failedtimestamp is too old/timestamp is in the future- replay protection (5-minute window by default)body is not valid JSONmissing event_type/missing event_id
Surface the error message back to the webhook sender as a 400; do not echo it to end users.