From d45eed07a42f422bb1a8648ef888c94e4b4e56cd Mon Sep 17 00:00:00 2001 From: catsika Date: Fri, 6 Nov 2026 16:02:47 +0000 Subject: [PATCH] feat: expose the API's errorCode on KeyMintApiError Every Keymint REST error now carries a stable string errorCode (e.g. license_invalid_or_forbidden, rate_limited) next to message and the numeric code. KeyMintApiError.ErrorCode exposes it; it is null when a response has none (older servers, non-JSON bodies). Existing properties and behaviour are unchanged. README: new Error Handling section that branches on ErrorCode instead of message text. --- CHANGELOG.md | 4 +++ README.md | 34 +++++++++++++++++++++++ Services/Types.cs | 2 ++ Tests/ApiContractTests.cs | 57 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe192f..21342c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- `KeyMintApiError.ErrorCode`: the stable string error code the API now sends with every error (for example `license_invalid_or_forbidden`, `rate_limited`). Branch on it instead of `Message`. `null` when the response has none; existing properties are unchanged. + ### Fixed - `KeyMintApiError.Error` now deserialises when the API sends `error` as a plain string (for example the 405 "Method not allowed" and the IP-allowlist 403) as well as an object. A string is kept in `Error.Message`; before, these responses failed to parse and returned a generic `HTTP error: ...` with `Code = -1`. diff --git a/README.md b/README.md index 0bec449..d20a229 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,40 @@ Keymint provides utilities to uniquely identify machines for node-locking: |-------------------------|--------------------------------------------------| | `VerifyWebhookSignature`| Verifies the signature of a webhook request payload. | +## Error Handling + +SDK methods don't throw for API errors. They return a `KeyMintResult`, and when `IsSuccess` is false, `Error` is a `KeyMintApiError` with `Message`, `Code`, `ErrorCode` and `Status`. Branch on `ErrorCode`, a stable string such as `license_invalid_or_forbidden` or `rate_limited`, not on `Message`, which is written for people and may be reworded. See the [error code reference](https://keymint.dev/docs/api-reference/error-codes) for the full list. + +```csharp +var result = await client.ActivateKey(new ActivateKeyParams { + ProductId = productId, + LicenseKey = licenseKey, + HostId = hostId +}); + +if (result.IsSuccess) { + Console.WriteLine($"Activated: {result.Data!.Message}"); +} else { + switch (result.Error!.ErrorCode) { + case "license_invalid_or_forbidden": + // Rejected: ask the user to re-enter the key or contact support. + break; + case "activation_limit_reached": + // Ask the user to deactivate another device. + break; + case "rate_limited": + case "service_unavailable": + // No answer yet: keep the last known state and retry later. + break; + default: + Console.WriteLine($"Keymint error {result.Error.ErrorCode ?? result.Error.Code.ToString()} (HTTP {result.Error.Status}): {result.Error.Message}"); + break; + } +} +``` + +`ErrorCode` is `null` when the response has none, for example a non-JSON body from a proxy or a network failure, where `Code` is `-1`. `Code` and `Error` (the legacy `error` field) are unchanged. + ## Idempotency All mutating SDK methods support idempotency keys to safely retry requests in case of network drops. Pass a `RequestOptions` instance as the optional second argument: diff --git a/Services/Types.cs b/Services/Types.cs index f3c62ce..78b9eb3 100644 --- a/Services/Types.cs +++ b/Services/Types.cs @@ -120,6 +120,8 @@ public class KeyMintApiError public string Message { get; set; } = "Keymint API request failed"; // Descriptive error message [JsonPropertyName("code")] public int Code { get; set; } // API numeric error code + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } // Stable string error code (e.g. "license_not_found"); branch on this, not on Message [JsonPropertyName("status")] public int? Status { get; set; } // HTTP status code, optional [JsonPropertyName("error")] diff --git a/Tests/ApiContractTests.cs b/Tests/ApiContractTests.cs index cb3dc92..5c2975b 100644 --- a/Tests/ApiContractTests.cs +++ b/Tests/ApiContractTests.cs @@ -373,6 +373,63 @@ public void Api_error_without_error_field_deserializes(string json) Assert.Null(error.Error); } + [Fact] + public async Task Api_errors_expose_error_code() + { + var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Content = new StringContent( + "{\"message\":\"Invalid license key or insufficient permissions\",\"code\":1,\"errorCode\":\"license_invalid_or_forbidden\"}", + Encoding.UTF8, + "application/json") + }); + var sdk = CreateSdk(handler); + + var result = await sdk.ActivateKey(new ActivateKeyParams { ProductId = "product_123", LicenseKey = "key_123" }); + + Assert.False(result.IsSuccess); + Assert.Equal("license_invalid_or_forbidden", result.Error!.ErrorCode); + Assert.Equal("Invalid license key or insufficient permissions", result.Error.Message); + Assert.Equal(1, result.Error.Code); + Assert.Equal(401, result.Error.Status); + } + + [Fact] + public async Task Api_errors_without_error_code_leave_it_null() + { + var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.TooManyRequests) + { + Content = new StringContent("{\"message\":\"Too many requests\",\"code\":3}", Encoding.UTF8, "application/json") + }); + var sdk = CreateSdk(handler); + + var result = await sdk.ActivateKey(new ActivateKeyParams { ProductId = "product_123", LicenseKey = "key_123" }); + + Assert.False(result.IsSuccess); + Assert.Null(result.Error!.ErrorCode); + Assert.Equal("Too many requests", result.Error.Message); + Assert.Equal(3, result.Error.Code); + Assert.Equal(429, result.Error.Status); + } + + [Fact] + public async Task Non_json_errors_have_no_error_code() + { + var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.BadGateway) + { + Content = new StringContent("Bad Gateway", Encoding.UTF8, "text/html") + }); + var sdk = CreateSdk(handler); + + var result = await sdk.ActivateKey(new ActivateKeyParams { ProductId = "product_123", LicenseKey = "key_123" }); + + Assert.False(result.IsSuccess); + Assert.Null(result.Error!.ErrorCode); + Assert.Equal(-1, result.Error.Code); + Assert.Equal(502, result.Error.Status); + Assert.StartsWith("HTTP error: BadGateway", result.Error.Message); + } + private static KeyMintSDK CreateSdk(HttpMessageHandler handler) { return new KeyMintSDK("admin_test", new HttpClient(handler)