Error Handling
Comprehensive guide to handling errors when integrating with the License Server API, including retry strategies, common error scenarios, and debugging techniques.
HTTP Status Codes
The License Server API uses standard HTTP status codes to indicate the success or failure of requests:
Success Codes
- 200 OK - Request successful
Client Error Codes
- 400 Bad Request - Invalid request parameters
- 403 Forbidden - Forbidden by a business rule: missing/invalid admin key, but also license-specific states like expired, revoked/inactive, or activation-limit-exceeded on otherwise-public endpoints
- 404 Not Found - License key or endpoint not found
- 409 Conflict - The license is already in the requested state (e.g. revoking an already-revoked license)
- 429 Too Many Requests - Rate limit exceeded
Server Error Codes
- 500 Internal Server Error - Server-side error
- 503 Service Unavailable - A concurrent-update conflict on
POST /v1/track-usageorPOST /v1/admin/reset-usagespecifically — both use optimistic locking on the license's usage counters, and lose that race under concurrent writes to the same key. Retrying the request is the correct response, per each route's own{"error": "Failed to track usage due to a concurrent update - please retry"}-shaped message.
The API never returns 201 Created — every successful write (issuing, revoking, activating, etc.) responds 200.
Error Response Format
Error responses are a plain {"error": "..."} object — there's no code or details field anywhere in the API:
{
"error": "Descriptive error message"
}A few specific endpoints add extra sibling fields alongside error for that one case (e.g. verify-license adds "valid": false; a 429 adds statusCode, message, and retryAfter — see below) — but there is no generic code/details envelope used API-wide. track-usage and validate-license don't add a valid field on error — track-usage's error responses are just {"error": "..."}, and validate-license always returns 200 {"valid": false} with no error field at all.
Common Error Scenarios
1. License Key Not Found
Scenario: License verification is called with a key that doesn't exist. There's no separate "invalid format" check — any string that isn't a real key gets the same 404, whether it's malformed or just wrong:
curl -X POST https://your-license-api.com/v1/verify-license \
-H "Content-Type: application/json" \
-d '{"key": "invalid-key-format"}'Response (404):
{
"error": "License key not found",
"valid": false
}Handling:
async function verifyLicense(key) {
try {
const response = await fetch('/v1/verify-license', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key })
});
const data = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${data.error}`);
}
return data;
} catch (error) {
if (error.message.includes('License key not found')) {
return { valid: false, error: 'Please check your license key' };
}
throw error;
}
}2. Expired License
Scenario: License exists but has expired.
Response (403):
{
"error": "License has expired",
"valid": false
}There's no expired boolean or expires_at field in the response — you have to branch on the error message text. Note also that a revoked (or otherwise inactive) license returns a different message, "License is not active", not anything containing the word "revoked":
Handling:
function handleLicenseVerification(result) {
if (!result.valid) {
if (result.error?.includes('expired')) {
showError('Your license has expired. Please renew your license to continue.');
} else if (result.error?.includes('not active')) {
showError('Your license is not active. Please contact support.');
} else {
showError('Invalid license key. Please check your key and try again.');
}
return false;
}
return true;
}3. Activation Limit Reached
Scenario: Trying to activate a license that has reached its maximum activations.
Response:
{
"error": "Activation limit exceeded for this license key"
}Handling:
async function activateLicense(key, instanceId) {
try {
const response = await fetch('/v1/activate-license', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, instance_id: instanceId })
});
const data = await response.json();
if (response.status === 403 && data.error === 'Activation limit exceeded for this license key') {
return {
success: false,
error: 'License activation limit reached. Please deactivate unused instances or upgrade your license.'
};
}
return { success: data.activated, data };
} catch (error) {
return { success: false, error: 'Network error during activation' };
}
}4. Rate Limiting
Scenario: Too many requests in a short time period.
Response (429):
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded. Try again in 60 seconds.",
"retryAfter": 60
}Note the field is retryAfter (camelCase), not retry_after — this is also mirrored in the Retry-After response header, which the example below reads instead of the body field.
Handling with Exponential Backoff:
class LicenseClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async makeRequest(endpoint, options = {}, retryCount = 0) {
const maxRetries = 3;
const baseDelay = 1000; // 1 second
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: { 'Content-Type': 'application/json' },
...options
});
if (response.status === 429) {
if (retryCount >= maxRetries) {
throw new Error('Max retries exceeded due to rate limiting');
}
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter ?
parseInt(retryAfter) * 1000 :
baseDelay * Math.pow(2, retryCount);
console.log(`Rate limited, retrying after ${delay}ms...`);
await this.sleep(delay);
return this.makeRequest(endpoint, options, retryCount + 1);
}
return response;
} catch (error) {
if (retryCount < maxRetries && this.isRetriableError(error)) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`Network error, retrying after ${delay}ms...`);
await this.sleep(delay);
return this.makeRequest(endpoint, options, retryCount + 1);
}
throw error;
}
}
isRetriableError(error) {
return error.name === 'TypeError' || // Network errors
error.message.includes('fetch');
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async verifyLicense(key) {
const response = await this.makeRequest('/verify-license', {
method: 'POST',
body: JSON.stringify({ key })
});
return response.json();
}
}5. Admin Authentication Errors
Scenario: Invalid or missing admin API key for protected endpoints.
Response:
{
"error": "Unauthorized"
}Handling:
class AdminClient {
constructor(baseUrl, adminKey) {
this.baseUrl = baseUrl;
this.adminKey = adminKey;
}
async makeAdminRequest(endpoint, options = {}) {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.adminKey}`,
...options.headers
},
...options
});
if (response.status === 403) {
throw new Error('Admin authentication failed. Please check your API key.');
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || `HTTP ${response.status}`);
}
return response.json();
} catch (error) {
if (error.message.includes('Admin authentication failed')) {
console.error('Invalid admin API key. Please verify your credentials.');
}
throw error;
}
}
}Comprehensive Error Handling Strategy
1. Circuit Breaker Pattern
Prevent cascading failures by temporarily stopping requests after repeated failures:
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
this.failureCount = 0;
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
}
this.failureCount = 0;
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
}
throw error;
}
}
}
// Usage
const circuitBreaker = new CircuitBreaker(3, 30000);
const licenseClient = new LicenseClient('https://your-license-api.com/v1');
async function verifyLicenseWithCircuitBreaker(key) {
try {
return await circuitBreaker.execute(() => licenseClient.verifyLicense(key));
} catch (error) {
if (error.message.includes('Circuit breaker is OPEN')) {
return {
valid: false,
error: 'License service temporarily unavailable. Please try again later.'
};
}
throw error;
}
}2. Timeout Handling
Prevent hanging requests with proper timeouts:
class TimeoutError extends Error {
constructor(message) {
super(message);
this.name = 'TimeoutError';
}
}
function withTimeout(promise, timeoutMs = 30000) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new TimeoutError(`Operation timed out after ${timeoutMs}ms`)), timeoutMs);
});
return Promise.race([promise, timeout]);
}
// Usage
async function verifyLicenseWithTimeout(key) {
try {
return await withTimeout(
fetch('/v1/verify-license', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key })
}).then(r => r.json()),
15000 // 15 second timeout
);
} catch (error) {
if (error instanceof TimeoutError) {
return {
valid: false,
error: 'License verification timed out. Please check your connection.'
};
}
throw error;
}
}3. Comprehensive Error Logger
class ErrorLogger {
constructor() {
this.logs = [];
}
logError(operation, error, context = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
operation,
error: {
message: error.message,
stack: error.stack,
name: error.name
},
context,
userAgent: navigator?.userAgent,
url: window?.location?.href
};
this.logs.push(logEntry);
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('License Error:', logEntry);
}
// Send to monitoring service in production
if (process.env.NODE_ENV === 'production') {
this.sendToMonitoring(logEntry);
}
}
async sendToMonitoring(logEntry) {
try {
await fetch('/api/error-tracking', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(logEntry)
});
} catch (err) {
console.error('Failed to send error to monitoring:', err);
}
}
getRecentErrors(count = 10) {
return this.logs.slice(-count);
}
}
const errorLogger = new ErrorLogger();
// Usage in license operations
async function verifyLicenseWithLogging(key) {
try {
return await licenseClient.verifyLicense(key);
} catch (error) {
errorLogger.logError('license_verification', error, { key: key.substring(0, 8) + '...' });
throw error;
}
}Error Recovery Strategies
1. Graceful Degradation
class LicenseManager {
constructor(apiUrl) {
this.apiUrl = apiUrl;
this.cachedLicense = this.loadCachedLicense();
this.gracePeriod = 24 * 60 * 60 * 1000; // 24 hours
}
async verifyLicense(key) {
try {
const result = await this.verifyOnline(key);
this.cacheLicense(result);
return result;
} catch (error) {
console.warn('Online verification failed, checking cached license:', error.message);
return this.verifyFromCache(key);
}
}
verifyFromCache(key) {
if (!this.cachedLicense || this.cachedLicense.key !== key) {
return { valid: false, error: 'No cached license available' };
}
const cacheAge = Date.now() - this.cachedLicense.timestamp;
if (cacheAge > this.gracePeriod) {
return {
valid: false,
error: 'Cached license expired. Please check your internet connection.'
};
}
return {
valid: true,
cached: true,
message: 'Using cached license verification',
...this.cachedLicense.data
};
}
cacheLicense(result) {
if (result.valid) {
this.cachedLicense = {
key: result.key,
data: result,
timestamp: Date.now()
};
localStorage.setItem('license_cache', JSON.stringify(this.cachedLicense));
}
}
loadCachedLicense() {
try {
const cached = localStorage.getItem('license_cache');
return cached ? JSON.parse(cached) : null;
} catch {
return null;
}
}
}2. User Notification System
class UserNotificationManager {
constructor() {
this.notifications = [];
}
showError(message, type = 'error', duration = 5000) {
const notification = {
id: Date.now(),
message,
type,
timestamp: new Date()
};
this.notifications.push(notification);
this.displayNotification(notification);
if (duration > 0) {
setTimeout(() => this.removeNotification(notification.id), duration);
}
return notification.id;
}
showLicenseError(error, licenseKey) {
let message;
let actions = [];
switch (error.type) {
case 'expired':
message = 'Your license has expired. Please renew to continue using the application.';
actions = [{ text: 'Renew License', action: () => this.openRenewalPage() }];
break;
case 'invalid':
message = 'Invalid license key. Please check your key and try again.';
actions = [{ text: 'Enter New Key', action: () => this.openLicenseDialog() }];
break;
case 'network':
message = 'Unable to verify license. Check your internet connection.';
actions = [{ text: 'Retry', action: () => this.retryVerification(licenseKey) }];
break;
case 'rate_limit':
message = 'Too many license verification attempts. Please wait and try again.';
break;
default:
message = `License error: ${error.message}`;
}
return this.showError(message, 'error', 0); // Don't auto-hide
}
displayNotification(notification) {
// Implementation depends on your UI framework
console.log(`[${notification.type.toUpperCase()}] ${notification.message}`);
}
removeNotification(id) {
this.notifications = this.notifications.filter(n => n.id !== id);
}
}Testing Error Scenarios
Unit Tests for Error Handling
// Using Jest
describe('License Error Handling', () => {
let licenseClient;
beforeEach(() => {
licenseClient = new LicenseClient('https://test-api.com');
});
test('handles unknown license key', async () => {
fetch.mockRejectOnce(new Error('License key not found'));
const result = await licenseClient.verifyLicense('unknown-key');
expect(result.valid).toBe(false);
expect(result.error).toContain('License key not found');
});
test('retries on rate limit with exponential backoff', async () => {
fetch
.mockRejectOnce(new Error('Too many requests'))
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ valid: true })
});
const result = await licenseClient.verifyLicense('valid-key');
expect(fetch).toHaveBeenCalledTimes(2);
expect(result.valid).toBe(true);
});
test('fails after max retries', async () => {
fetch.mockReject(new Error('Network error'));
await expect(licenseClient.verifyLicense('any-key'))
.rejects.toThrow('Max retries exceeded');
});
});Monitoring and Alerting
Health Check Implementation
class LicenseHealthMonitor {
// apiBaseUrl is the server's root, e.g. "https://your-license-api.com" -
// no method named healthCheck() exists on any client class; this hits
// the real, unauthenticated GET / health route directly (see
// Health Endpoints in the API Reference — that route has no /v1 prefix).
constructor(apiBaseUrl) {
this.apiBaseUrl = apiBaseUrl;
this.healthStatus = 'unknown';
this.lastCheck = null;
this.errorCount = 0;
this.successCount = 0;
}
async performHealthCheck() {
try {
const start = Date.now();
const response = await fetch(this.apiBaseUrl);
if (!response.ok) throw new Error(`Health check failed: ${response.status}`);
const duration = Date.now() - start;
this.healthStatus = 'healthy';
this.successCount++;
this.lastCheck = new Date();
return {
status: 'healthy',
responseTime: duration,
lastCheck: this.lastCheck
};
} catch (error) {
this.healthStatus = 'unhealthy';
this.errorCount++;
this.lastCheck = new Date();
return {
status: 'unhealthy',
error: error.message,
lastCheck: this.lastCheck,
errorCount: this.errorCount
};
}
}
getHealthMetrics() {
return {
status: this.healthStatus,
lastCheck: this.lastCheck,
errorCount: this.errorCount,
successCount: this.successCount,
uptime: this.successCount / (this.successCount + this.errorCount)
};
}
}Best Practices
Error Categorization
- Transient errors: Network issues, rate limits, server errors - retry
- Permanent errors: Invalid keys, expired licenses, auth failures - don't retry
- Degraded service: Use cached data, show warnings
For general integration best practices (performance, security, monitoring, UX), see the Integration Checklist.
Next Steps
- Integration patterns for robust license checking
- API reference for complete error response documentation
- Rate limits guide for avoiding 429 errors
