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
- 201 Created - Resource created successfully (rare in this API)
Client Error Codes
- 400 Bad Request - Invalid request parameters
- 403 Forbidden - Admin authentication required but missing/invalid
- 404 Not Found - Endpoint or resource not found
- 429 Too Many Requests - Rate limit exceeded
Server Error Codes
- 500 Internal Server Error - Server-side error
- 503 Service Unavailable - Server temporarily unavailable
Error Response Format
All error responses follow a consistent JSON format:
{
"error": "Descriptive error message",
"code": "ERROR_CODE_IF_APPLICABLE",
"details": {
"additional": "context information"
}
}Common Error Scenarios
1. Invalid License Key
Scenario: License verification fails due to invalid key format or non-existent key.
curl -X POST https://your-license-api.com/v1/verify-license \
-H "Content-Type: application/json" \
-d '{"key": "invalid-key-format"}'Response:
{
"error": "Invalid license key format",
"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('Invalid license key format')) {
return { valid: false, error: 'Please check your license key format' };
}
throw error;
}
}2. Expired License
Scenario: License exists but has expired.
Response:
{
"error": "License expired",
"valid": false,
"expired": true,
"expires_at": "2024-01-01T00:00:00Z"
}Handling:
function handleLicenseVerification(result) {
if (!result.valid) {
if (result.expired) {
showError('Your license has expired. Please renew your license to continue.');
} else if (result.revoked) {
showError('Your license has been revoked. 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:
{
"error": "Too many requests",
"retry_after": 60
}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 invalid license key format', async () => {
fetch.mockRejectOnce(new Error('Invalid license key format'));
const result = await licenseClient.verifyLicense('invalid-key');
expect(result.valid).toBe(false);
expect(result.error).toContain('Invalid license key format');
});
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 {
constructor(licenseClient) {
this.licenseClient = licenseClient;
this.healthStatus = 'unknown';
this.lastCheck = null;
this.errorCount = 0;
this.successCount = 0;
}
async performHealthCheck() {
try {
const start = Date.now();
const result = await this.licenseClient.healthCheck();
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
1. 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
2. User Experience
- Show meaningful error messages
- Provide actionable next steps
- Don't expose technical details to end users
- Cache successful verifications for offline scenarios
3. Monitoring
- Log all license-related errors
- Set up alerts for error rate spikes
- Monitor license verification success rates
- Track error patterns and categories
4. Security Considerations
- Don't log full license keys
- Sanitize error messages in production
- Implement rate limiting for retry attempts
- Secure error reporting endpoints
Next Steps
- Integration patterns for robust license checking
- API reference for complete error response documentation
- Rate limits guide for avoiding 429 errors