@onlineapps/error-handler-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/jest.config.js +23 -0
- package/onlineapps-error-handler-core-1.0.0.tgz +0 -0
- package/package.json +34 -0
- package/src/CircuitBreaker.js +110 -0
- package/src/CompensationHandler.js +104 -0
- package/src/DLQRouter.js +98 -0
- package/src/ErrorClassifier.js +142 -0
- package/src/ErrorHandlerCore.js +329 -0
- package/src/RetryHandler.js +74 -0
- package/src/UnifiedLogSchema.js +105 -0
- package/src/index.js +30 -0
- package/tests/unit/DLQRouter.test.js +94 -0
- package/tests/unit/ErrorClassifier.test.js +82 -0
- package/tests/unit/ErrorHandlerCore.test.js +183 -0
- package/tests/unit/RetryHandler.test.js +81 -0
- package/tests/unit/UnifiedLogSchema.test.js +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @onlineapps/error-handler-core
|
|
2
|
+
|
|
3
|
+
Core error handling functionality for OA Drive microservices. Used by both business services (via `conn-infra-error-handler` wrapper) and infrastructure services (directly).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Error Classification** - Automatic error type detection (TRANSIENT, BUSINESS, FATAL, etc.)
|
|
8
|
+
- **Retry Logic** - Exponential backoff for transient errors
|
|
9
|
+
- **Circuit Breaker** - Protection against cascading failures
|
|
10
|
+
- **DLQ Routing** - Dead letter queue routing for permanent errors
|
|
11
|
+
- **Compensation** - Rollback operations for failed workflows
|
|
12
|
+
- **Unified Logging** - Structured error logging via monitoring-core
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @onlineapps/error-handler-core
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic Setup
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
const { init: initMonitoring } = require('@onlineapps/monitoring-core');
|
|
26
|
+
const { ErrorHandlerCore } = require('@onlineapps/error-handler-core');
|
|
27
|
+
|
|
28
|
+
// Initialize monitoring-core
|
|
29
|
+
const monitoring = await initMonitoring({
|
|
30
|
+
serviceName: 'my-service',
|
|
31
|
+
mode: 'light'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Initialize error handler
|
|
35
|
+
const errorHandler = new ErrorHandlerCore({
|
|
36
|
+
serviceName: 'my-service',
|
|
37
|
+
serviceVersion: '1.0.0',
|
|
38
|
+
environment: process.env.NODE_ENV,
|
|
39
|
+
monitoring: monitoring, // monitoring-core instance (required)
|
|
40
|
+
handling: {
|
|
41
|
+
maxRetries: 3,
|
|
42
|
+
retryDelay: 1000,
|
|
43
|
+
retryMultiplier: 2,
|
|
44
|
+
circuitBreakerEnabled: true,
|
|
45
|
+
dlqEnabled: true,
|
|
46
|
+
mqClient: mqClient // Optional, for DLQ routing
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Log Error
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
await errorHandler.logError({
|
|
55
|
+
moduleName: 'MyModule',
|
|
56
|
+
operation: 'myOperation',
|
|
57
|
+
error: new Error('Something failed'),
|
|
58
|
+
context: { userId: 123 }
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Handle Error
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
try {
|
|
66
|
+
await criticalOperation();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const result = await errorHandler.handleError({
|
|
69
|
+
moduleName: 'MyModule',
|
|
70
|
+
operation: 'criticalOperation',
|
|
71
|
+
error: error,
|
|
72
|
+
context: { operationId: 'op-123' }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// result.action: 'retry' | 'dlq' | 'throw' | 'continue' | 'compensate'
|
|
76
|
+
if (result.action === 'retry') {
|
|
77
|
+
await sleep(result.retryDelay);
|
|
78
|
+
// Retry operation
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Execute with Retry
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
const result = await errorHandler.executeWithRetry({
|
|
87
|
+
moduleName: 'MQClient',
|
|
88
|
+
operation: 'publish',
|
|
89
|
+
fn: async () => await mqClient.publish(queue, message),
|
|
90
|
+
context: { queue, messageId }
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Execute with Circuit Breaker
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
const result = await errorHandler.executeWithCircuitBreaker({
|
|
98
|
+
moduleName: 'ExternalAPI',
|
|
99
|
+
operation: 'getUser',
|
|
100
|
+
circuitName: 'user-api',
|
|
101
|
+
fn: async () => await userAPI.getUser(id),
|
|
102
|
+
context: { userId: id }
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API Reference
|
|
107
|
+
|
|
108
|
+
See [UNIFIED_ERROR_HANDLING.md](/docs/standards/UNIFIED_ERROR_HANDLING.md) for complete API documentation.
|
|
109
|
+
|
|
110
|
+
## Architecture
|
|
111
|
+
|
|
112
|
+
- **Core Library** - Used directly by infrastructure services
|
|
113
|
+
- **Wrapper** - `conn-infra-error-handler` wraps this for business services
|
|
114
|
+
- **Dependencies** - Uses `monitoring-core` for logging
|
|
115
|
+
|
|
116
|
+
## Related Documentation
|
|
117
|
+
|
|
118
|
+
- [Unified Error Handling Standard](/docs/standards/UNIFIED_ERROR_HANDLING.md)
|
|
119
|
+
- [Error Handling Architecture](/docs/standards/ERROR_HANDLING_ARCHITECTURE.md)
|
|
120
|
+
- [Error Handling Standard](/docs/standards/ERROR_HANDLING.md)
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
*Version: 1.0.0 | License: MIT*
|
|
125
|
+
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
coveragePathIgnorePatterns: [
|
|
4
|
+
'/node_modules/',
|
|
5
|
+
'/tests/'
|
|
6
|
+
],
|
|
7
|
+
testMatch: [
|
|
8
|
+
'**/tests/**/*.test.js'
|
|
9
|
+
],
|
|
10
|
+
collectCoverageFrom: [
|
|
11
|
+
'src/**/*.js',
|
|
12
|
+
'!src/**/*.test.js'
|
|
13
|
+
],
|
|
14
|
+
coverageThreshold: {
|
|
15
|
+
global: {
|
|
16
|
+
branches: 70,
|
|
17
|
+
functions: 70,
|
|
18
|
+
lines: 70,
|
|
19
|
+
statements: 70
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onlineapps/error-handler-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core error handling functionality for OA Drive microservices - classification, retry, circuit breaker, DLQ routing",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:coverage": "jest --coverage",
|
|
9
|
+
"test:watch": "jest --watch",
|
|
10
|
+
"test:unit": "jest tests/unit",
|
|
11
|
+
"test:component": "jest tests/component"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"error-handling",
|
|
15
|
+
"retry",
|
|
16
|
+
"circuit-breaker",
|
|
17
|
+
"dlq",
|
|
18
|
+
"microservices"
|
|
19
|
+
],
|
|
20
|
+
"author": "OnlineApps",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@onlineapps/monitoring-core": "^1.0.0",
|
|
24
|
+
"opossum": "^7.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"jest": "^29.7.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"registry": "https://registry.npmjs.org/",
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker
|
|
3
|
+
* Protects against cascading failures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CircuitBreaker = require('opossum');
|
|
7
|
+
|
|
8
|
+
class CircuitBreakerManager {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.enabled = config.circuitBreakerEnabled !== false;
|
|
11
|
+
this.options = {
|
|
12
|
+
timeout: config.timeout || 10000,
|
|
13
|
+
errorThresholdPercentage: config.errorThreshold || 50,
|
|
14
|
+
resetTimeout: config.resetTimeout || 30000,
|
|
15
|
+
rollingCountTimeout: config.rollingCountTimeout || 10000,
|
|
16
|
+
rollingCountBuckets: config.rollingCountBuckets || 10,
|
|
17
|
+
...config.circuitBreakerOptions
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Registry of circuit breakers
|
|
21
|
+
this.breakers = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get or create circuit breaker for operation
|
|
26
|
+
*
|
|
27
|
+
* @param {string} name - Operation name
|
|
28
|
+
* @param {Function} fn - Function to protect
|
|
29
|
+
* @param {Object} options - Circuit breaker options
|
|
30
|
+
* @returns {CircuitBreaker} Circuit breaker instance
|
|
31
|
+
*/
|
|
32
|
+
getCircuitBreaker(name, fn, options = {}) {
|
|
33
|
+
if (!this.enabled) {
|
|
34
|
+
// Return pass-through if disabled
|
|
35
|
+
return {
|
|
36
|
+
fire: () => fn(),
|
|
37
|
+
opened: false,
|
|
38
|
+
halfOpen: false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!this.breakers.has(name)) {
|
|
43
|
+
const breaker = new CircuitBreaker(fn, {
|
|
44
|
+
...this.options,
|
|
45
|
+
...options
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.breakers.set(name, breaker);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return this.breakers.get(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Execute function with circuit breaker protection
|
|
56
|
+
*
|
|
57
|
+
* @param {string} name - Operation name
|
|
58
|
+
* @param {Function} fn - Async function to execute
|
|
59
|
+
* @param {Object} options - Circuit breaker options
|
|
60
|
+
* @returns {Promise<*>} Function result
|
|
61
|
+
* @throws {Error} If circuit is open or operation fails
|
|
62
|
+
*/
|
|
63
|
+
async executeWithCircuitBreaker(name, fn, options = {}) {
|
|
64
|
+
const breaker = this.getCircuitBreaker(name, fn, options);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return await breaker.fire();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.code === 'EOPENBREAKER') {
|
|
70
|
+
throw new Error(`Service unavailable: ${name} circuit is open`);
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get circuit breaker state
|
|
78
|
+
*
|
|
79
|
+
* @param {string} name - Circuit breaker name
|
|
80
|
+
* @returns {string} State: 'open' | 'half-open' | 'closed'
|
|
81
|
+
*/
|
|
82
|
+
getState(name) {
|
|
83
|
+
if (!this.breakers.has(name)) {
|
|
84
|
+
return 'closed';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const breaker = this.breakers.get(name);
|
|
88
|
+
if (breaker.opened) return 'open';
|
|
89
|
+
if (breaker.halfOpen) return 'half-open';
|
|
90
|
+
return 'closed';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get all circuit breaker states
|
|
95
|
+
* @returns {Object} Map of circuit breaker states
|
|
96
|
+
*/
|
|
97
|
+
getAllStates() {
|
|
98
|
+
const states = {};
|
|
99
|
+
this.breakers.forEach((breaker, name) => {
|
|
100
|
+
states[name] = {
|
|
101
|
+
state: this.getState(name),
|
|
102
|
+
stats: breaker.stats || {}
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
return states;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = CircuitBreakerManager;
|
|
110
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compensation Handler
|
|
3
|
+
* Executes rollback operations for failed workflow steps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class CompensationHandler {
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.enabled = config.compensationEnabled !== false;
|
|
9
|
+
this.handlers = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register compensation handler for operation
|
|
14
|
+
*
|
|
15
|
+
* @param {string} operation - Operation name
|
|
16
|
+
* @param {Function} handler - Compensation handler function
|
|
17
|
+
*/
|
|
18
|
+
register(operation, handler) {
|
|
19
|
+
if (typeof handler !== 'function') {
|
|
20
|
+
throw new Error(`Compensation handler for ${operation} must be a function`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.handlers.set(operation, handler);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Execute compensation for failed operation
|
|
28
|
+
*
|
|
29
|
+
* @param {string} operation - Operation that failed
|
|
30
|
+
* @param {Object} context - Operation context for compensation
|
|
31
|
+
* @returns {Promise<*>} Compensation result or null if no handler
|
|
32
|
+
* @throws {Error} If compensation fails
|
|
33
|
+
*/
|
|
34
|
+
async execute(operation, context) {
|
|
35
|
+
if (!this.enabled) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handler = this.handlers.get(operation);
|
|
40
|
+
|
|
41
|
+
if (!handler) {
|
|
42
|
+
return null; // No compensation handler registered
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return await handler(context);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new Error(`Compensation failed for ${operation}: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute compensations in reverse order
|
|
54
|
+
* Used for workflow rollback
|
|
55
|
+
*
|
|
56
|
+
* @param {Array<string>} operations - Operations executed (in order)
|
|
57
|
+
* @param {Object} context - Shared context for all compensations
|
|
58
|
+
* @returns {Promise<Array>} Results of all compensations
|
|
59
|
+
*/
|
|
60
|
+
async executeReverse(operations, context) {
|
|
61
|
+
if (!this.enabled || !operations || operations.length === 0) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const results = [];
|
|
66
|
+
|
|
67
|
+
// Execute in reverse order
|
|
68
|
+
for (let i = operations.length - 1; i >= 0; i--) {
|
|
69
|
+
const operation = operations[i];
|
|
70
|
+
try {
|
|
71
|
+
const result = await this.execute(operation, context);
|
|
72
|
+
if (result !== null) {
|
|
73
|
+
results.push({ operation, success: true, result });
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
results.push({ operation, success: false, error: error.message });
|
|
77
|
+
// Continue with other compensations even if one fails
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if compensation handler exists for operation
|
|
86
|
+
*
|
|
87
|
+
* @param {string} operation - Operation name
|
|
88
|
+
* @returns {boolean} True if handler exists
|
|
89
|
+
*/
|
|
90
|
+
hasHandler(operation) {
|
|
91
|
+
return this.handlers.has(operation);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get all registered operations
|
|
96
|
+
* @returns {Array<string>} List of operation names with handlers
|
|
97
|
+
*/
|
|
98
|
+
getRegisteredOperations() {
|
|
99
|
+
return Array.from(this.handlers.keys());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = CompensationHandler;
|
|
104
|
+
|
package/src/DLQRouter.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead Letter Queue Router
|
|
3
|
+
* Routes failed messages to DLQ for manual inspection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class DLQRouter {
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.enabled = config.dlqEnabled !== false;
|
|
9
|
+
this.mqClient = config.mqClient || null;
|
|
10
|
+
this.dlqNamePattern = config.dlqNamePattern || '{queue}.dlq';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Route message to DLQ
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} message - Original message
|
|
17
|
+
* @param {Error} error - Error that occurred
|
|
18
|
+
* @param {string} errorType - Classified error type
|
|
19
|
+
* @param {Object} context - Additional context
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
async routeToDLQ(message, error, errorType, context = {}) {
|
|
23
|
+
if (!this.enabled || !this.mqClient) {
|
|
24
|
+
return; // DLQ disabled or no MQ client
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const originalQueue = message.queue || context.queue || 'unknown';
|
|
28
|
+
const dlqName = this._generateDLQName(originalQueue);
|
|
29
|
+
|
|
30
|
+
const dlqMessage = {
|
|
31
|
+
// Original message
|
|
32
|
+
...message,
|
|
33
|
+
|
|
34
|
+
// Error details
|
|
35
|
+
error: {
|
|
36
|
+
message: error.message,
|
|
37
|
+
stack: error.stack,
|
|
38
|
+
code: error.code || null,
|
|
39
|
+
type: errorType,
|
|
40
|
+
timestamp: new Date().toISOString()
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Routing metadata
|
|
44
|
+
originalQueue: originalQueue,
|
|
45
|
+
retryCount: message.retryCount || context.retryCount || 0,
|
|
46
|
+
routedAt: new Date().toISOString(),
|
|
47
|
+
routingReason: this._determineRoutingReason(errorType, context)
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await this.mqClient.publish(dlqName, dlqMessage, {
|
|
52
|
+
persistent: true
|
|
53
|
+
});
|
|
54
|
+
} catch (dlqError) {
|
|
55
|
+
// If DLQ routing fails, log but don't throw
|
|
56
|
+
// (we don't want DLQ failures to break error handling)
|
|
57
|
+
console.error(`[DLQRouter] Failed to route to DLQ ${dlqName}:`, dlqError.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate DLQ name from original queue name
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
_generateDLQName(originalQueue) {
|
|
66
|
+
if (this.dlqNamePattern.includes('{queue}')) {
|
|
67
|
+
return this.dlqNamePattern.replace('{queue}', originalQueue);
|
|
68
|
+
}
|
|
69
|
+
return this.dlqNamePattern;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Determine routing reason
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
_determineRoutingReason(errorType, context) {
|
|
77
|
+
if (context.retryCount >= context.maxRetries) {
|
|
78
|
+
return 'max_retries_exceeded';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (['BUSINESS', 'VALIDATION'].includes(errorType)) {
|
|
82
|
+
return 'permanent_error';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return 'error_handling_decision';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if DLQ routing is available
|
|
90
|
+
* @returns {boolean} True if DLQ is enabled and MQ client available
|
|
91
|
+
*/
|
|
92
|
+
isAvailable() {
|
|
93
|
+
return this.enabled && this.mqClient !== null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = DLQRouter;
|
|
98
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Classifier
|
|
3
|
+
* Classifies errors into types for appropriate handling strategy
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Error types enum
|
|
7
|
+
const ErrorTypes = {
|
|
8
|
+
TRANSIENT: 'TRANSIENT', // Network, temporary failures - RETRY
|
|
9
|
+
BUSINESS: 'BUSINESS', // Business logic errors - DLQ
|
|
10
|
+
FATAL: 'FATAL', // Critical failures - THROW immediately
|
|
11
|
+
VALIDATION: 'VALIDATION', // Input validation - DLQ
|
|
12
|
+
TIMEOUT: 'TIMEOUT', // Operation timeout - RETRY
|
|
13
|
+
RATE_LIMIT: 'RATE_LIMIT', // Rate limiting - RETRY with backoff
|
|
14
|
+
UNKNOWN: 'UNKNOWN' // Unclassified - LOG and THROW
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Error code mappings
|
|
18
|
+
const ErrorCodes = {
|
|
19
|
+
// Network errors → TRANSIENT
|
|
20
|
+
ECONNREFUSED: ErrorTypes.TRANSIENT,
|
|
21
|
+
ENOTFOUND: ErrorTypes.TRANSIENT,
|
|
22
|
+
ETIMEDOUT: ErrorTypes.TRANSIENT,
|
|
23
|
+
ECONNRESET: ErrorTypes.TRANSIENT,
|
|
24
|
+
EPIPE: ErrorTypes.TRANSIENT,
|
|
25
|
+
|
|
26
|
+
// HTTP status codes
|
|
27
|
+
408: ErrorTypes.TIMEOUT,
|
|
28
|
+
429: ErrorTypes.RATE_LIMIT,
|
|
29
|
+
500: ErrorTypes.TRANSIENT,
|
|
30
|
+
502: ErrorTypes.TRANSIENT,
|
|
31
|
+
503: ErrorTypes.TRANSIENT,
|
|
32
|
+
504: ErrorTypes.TIMEOUT,
|
|
33
|
+
|
|
34
|
+
// Business errors
|
|
35
|
+
400: ErrorTypes.VALIDATION,
|
|
36
|
+
401: ErrorTypes.BUSINESS,
|
|
37
|
+
403: ErrorTypes.BUSINESS,
|
|
38
|
+
404: ErrorTypes.BUSINESS,
|
|
39
|
+
409: ErrorTypes.BUSINESS,
|
|
40
|
+
422: ErrorTypes.VALIDATION
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
class ErrorClassifier {
|
|
44
|
+
/**
|
|
45
|
+
* Classify error into type for appropriate handling
|
|
46
|
+
*
|
|
47
|
+
* @param {Error} error - Error to classify
|
|
48
|
+
* @returns {string} Error type from ErrorTypes enum
|
|
49
|
+
*/
|
|
50
|
+
classify(error) {
|
|
51
|
+
if (!error) {
|
|
52
|
+
return ErrorTypes.UNKNOWN;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 1. Check explicit type
|
|
56
|
+
if (error.type && ErrorTypes[error.type]) {
|
|
57
|
+
return error.type;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Check error code
|
|
61
|
+
if (error.code && ErrorCodes[error.code]) {
|
|
62
|
+
return ErrorCodes[error.code];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Check HTTP status
|
|
66
|
+
if (error.statusCode && ErrorCodes[error.statusCode]) {
|
|
67
|
+
return ErrorCodes[error.statusCode];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. Check message patterns
|
|
71
|
+
const message = (error.message || '').toLowerCase();
|
|
72
|
+
|
|
73
|
+
if (message.includes('timeout')) {
|
|
74
|
+
return ErrorTypes.TIMEOUT;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (message.includes('rate limit') || message.includes('rate_limit')) {
|
|
78
|
+
return ErrorTypes.RATE_LIMIT;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (message.includes('validation') || message.includes('invalid')) {
|
|
82
|
+
return ErrorTypes.VALIDATION;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (message.includes('fatal') || message.includes('critical')) {
|
|
86
|
+
return ErrorTypes.FATAL;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (message.includes('connection') || message.includes('network') || message.includes('econn')) {
|
|
90
|
+
return ErrorTypes.TRANSIENT;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 5. Default
|
|
94
|
+
return ErrorTypes.UNKNOWN;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Determine if error should be retried
|
|
99
|
+
*
|
|
100
|
+
* @param {Error} error - Error to check
|
|
101
|
+
* @param {number} attempts - Current attempt count
|
|
102
|
+
* @param {number} maxRetries - Maximum retry attempts
|
|
103
|
+
* @returns {boolean} True if should retry
|
|
104
|
+
*/
|
|
105
|
+
shouldRetry(error, attempts = 0, maxRetries = 3) {
|
|
106
|
+
if (attempts >= maxRetries) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const errorType = this.classify(error);
|
|
111
|
+
|
|
112
|
+
// Only retry transient, timeout, and rate limit errors
|
|
113
|
+
return [
|
|
114
|
+
ErrorTypes.TRANSIENT,
|
|
115
|
+
ErrorTypes.TIMEOUT,
|
|
116
|
+
ErrorTypes.RATE_LIMIT
|
|
117
|
+
].includes(errorType);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get error types enum
|
|
122
|
+
* @returns {Object} ErrorTypes enum
|
|
123
|
+
*/
|
|
124
|
+
static getErrorTypes() {
|
|
125
|
+
return ErrorTypes;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get error codes mapping
|
|
130
|
+
* @returns {Object} ErrorCodes mapping
|
|
131
|
+
*/
|
|
132
|
+
static getErrorCodes() {
|
|
133
|
+
return ErrorCodes;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
ErrorClassifier,
|
|
139
|
+
ErrorTypes,
|
|
140
|
+
ErrorCodes
|
|
141
|
+
};
|
|
142
|
+
|