@onlineapps/error-handler-core 3.1.0 → 4.0.1
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/CHANGELOG.md +79 -0
- package/README.md +65 -2
- package/package.json +2 -2
- package/src/ErrorClassifier.js +49 -27
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,85 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [4.0.1] — 2026-10-12
|
|
8
|
+
|
|
9
|
+
pin: `@onlineapps/logger-contract` 2.0.0 → 2.1.0
|
|
10
|
+
|
|
11
|
+
## [4.0.0] — 2026-09-27
|
|
12
|
+
|
|
13
|
+
### Changed — BREAKING — `ErrorClassifier.classify()` už nečte `error.message`: třídu dává jen `type`, `code` a `status` (d.761)
|
|
14
|
+
|
|
15
|
+
Vydaná 3.1.0 měla ve `classify()` čtvrtý krok, který po `type`, `code` a `status`
|
|
16
|
+
hledal v `error.message` (převedeném na malá písmena) podřetězce: `timeout` →
|
|
17
|
+
`TIMEOUT`; `rate limit` / `rate_limit` → `RATE_LIMIT`; `validation` / `invalid` →
|
|
18
|
+
`VALIDATION`; `fatal` / `critical` → `FATAL`; `connection` / `network` / `econn` →
|
|
19
|
+
`TRANSIENT`. Ten krok je pryč. Chyba, kterou nedeklaruje žádné z těch tří polí, je
|
|
20
|
+
`UNKNOWN` bez ohledu na znění — a `UNKNOWN` se neopakuje a loguje se se závažností
|
|
21
|
+
`error`.
|
|
22
|
+
|
|
23
|
+
Důvod: hláška je lidský řetězec, podle kterého kontrakt chyby neklasifikuje, a čtení
|
|
24
|
+
znění byla druhá kolej vedle tabulky kódů (`change-discipline.md` § One rail per
|
|
25
|
+
concern) — každé přeformulování hlášky tiše měnilo třídu. Měření živých producentů
|
|
26
|
+
nenašlo nikoho, kdo by na krok dosáhl (`DeliveryDispatcher._canRetry` skládá
|
|
27
|
+
`{ status, code }` bez hlášky, orchestrátor posílá `type`/`status`/`code`,
|
|
28
|
+
`conn-infra-error-handler` jen deleguje). Jediný zásah byla vnitřní výjimka služby:
|
|
29
|
+
`TypeError` se slovem „invalid" se zařadil jako `VALIDATION`, a tím dostal závažnost
|
|
30
|
+
`warn` — vada našeho kódu podaná jako varování o vstupu. Teď je `UNKNOWN`, tedy
|
|
31
|
+
`error`.
|
|
32
|
+
|
|
33
|
+
Migrace: volající, který chce konkrétní třídu, ji deklaruje strojově — `error.type`
|
|
34
|
+
(hodnota z `ErrorTypes`), `error.code` z tabulky `ErrorCodes`, nebo `error.status`.
|
|
35
|
+
Kontrolní případy drží: `CIRCUIT_OPEN` → `TRANSIENT`, `422` → `VALIDATION`,
|
|
36
|
+
`503` → `TRANSIENT`.
|
|
37
|
+
|
|
38
|
+
### Fixed — čtyři změřené tvary chyb přenosu mají kategorii (d.774)
|
|
39
|
+
|
|
40
|
+
V tabulce `ErrorCodes` chyběly, takže odpovídaly `UNKNOWN` — a `UNKNOWN` se
|
|
41
|
+
neopakuje, doručení tedy skončilo bez jediného dalšího pokusu:
|
|
42
|
+
|
|
43
|
+
- `EAI_AGAIN` (resolver říká „zeptej se znovu", `getaddrinfo`), `ENETUNREACH`,
|
|
44
|
+
`EHOSTUNREACH` (směrování) → `TRANSIENT` — tatáž rodina jako `ECONNREFUSED`:
|
|
45
|
+
protistrana tam příště být může;
|
|
46
|
+
- `ECONNABORTED` (vypršená lhůta axiosu, který požadavek přeruší sám, proto bez
|
|
47
|
+
`cause` a bez syscallu) → `TIMEOUT`.
|
|
48
|
+
|
|
49
|
+
Tvary jsou změřené na skutečných socketech
|
|
50
|
+
(`infra/api_delivery_dispatcher/tests/unit/transportErrorShapes.test.js`). Nic jiného
|
|
51
|
+
se v tabulce nehnulo: `ECONNREFUSED`, `ENOTFOUND`, `ETIMEDOUT`, `ECONNRESET`, `EPIPE`
|
|
52
|
+
a `CIRCUIT_OPEN` drží svou třídu; trvalé tvary (`EPROTO`, `ERR_INVALID_URL`,
|
|
53
|
+
`ERR_BAD_REQUEST`, certifikátové) a `ERR_CANCELED` zůstávají `UNKNOWN`.
|
|
54
|
+
|
|
55
|
+
### Documentation — neznámý business kód se neoznačuje ani neodmítá (d.804)
|
|
56
|
+
|
|
57
|
+
README (§ A business code this table does not know) a JSDoc `classify()` říkají
|
|
58
|
+
pravidlo, které kód už měl: jméno business kódu, které tabulka nezná (např.
|
|
59
|
+
`REFERENCE_RECORD_MISSING`), mine vyhledání podle `code` a třídu dá další deklarované
|
|
60
|
+
pole — `error.type`, `error.code` v tabulce, `error.status`. Neznámé jméno na `422` je
|
|
61
|
+
tedy `VALIDATION` jako holé `422`, na `503` `TRANSIENT`; `error.retryable` klasifikátor
|
|
62
|
+
nečte. Slovník jmen business kódů tomuto balíčku nepatří — vlastní ho
|
|
63
|
+
`api/docs/biz/70-contracts/error-handling.md` §1.
|
|
64
|
+
|
|
65
|
+
### Documentation — `handleError()` bere záznam, ne holou chybu (d.804b)
|
|
66
|
+
|
|
67
|
+
README říká, že `handleError()` vyžaduje tatáž tři pole jako `logError()` —
|
|
68
|
+
`moduleName`, `operation` a `error` (`context` je jediný nepovinný klíč) — a že chybějící
|
|
69
|
+
pole schéma odmítne jménem, v pořadí `moduleName` → `operation` → `error`. Volající,
|
|
70
|
+
který předá zachycenou chybu samotnou, dostane doslova:
|
|
71
|
+
`[UnifiedLogSchema] Missing required field - "moduleName" must be a non-empty string naming the module that failed; it is the caller's to provide and is never invented. Fix: pass params.moduleName (ErrorHandlerCore takes it from errorData.moduleName).`
|
|
72
|
+
|
|
73
|
+
### Documentation — tabulka kódů je doména HTTP a Node, verdikt AMQP má jiného vlastníka (d.718)
|
|
74
|
+
|
|
75
|
+
README (§ Which codes the classifier answers for): `ErrorClassifier` čte jen vlastní
|
|
76
|
+
`ErrorTypes` (`error.type`), síťové kódy Node (`ECONNREFUSED`, `ETIMEDOUT`, …) a stavové
|
|
77
|
+
kódy HTTP (`error.code` / `error.status`) — `502`, `503` a `504` tu mají význam HTTP.
|
|
78
|
+
Selhání publikace klasifikuje transport, než chyba opustí
|
|
79
|
+
`@onlineapps/mq-client-core` (`src/utils/publishErrors.js`).
|
|
80
|
+
|
|
81
|
+
### Documentation — README nepíše verzi balíčku rukou (d.730)
|
|
82
|
+
|
|
83
|
+
Patička README tvrdila `*Version: 1.0.0 | License: MIT*`; verzi vlastní
|
|
84
|
+
`package.json`, patička je teď `*License: MIT*`.
|
|
85
|
+
|
|
7
86
|
## [3.1.0] — 2026-09-25
|
|
8
87
|
|
|
9
88
|
### Added — třída `ErrorTypes.DECLINED`: pravidlo práci zakázalo (d.831)
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Uniform: [library/connector](../connector/conn-orch-validator/manifests/library.
|
|
|
6
6
|
|
|
7
7
|
Duty sections that apply:
|
|
8
8
|
|
|
9
|
-
- `all`: L-MAIN, L-ENGINES, L-TESTS, L-TEST-SCRIPT, L-PACK-TESTS, L-PINS, L-NO-FILE-RANGE, L-CHANGELOG, L-README, L-README-REGION, L-CONSUMER
|
|
9
|
+
- `all`: L-MAIN, L-ENGINES, L-TESTS, L-TEST-SCRIPT, L-PACK-TESTS, L-PINS, L-NO-FILE-RANGE, L-CHANGELOG, L-PACK-CHANGELOG, L-README, L-README-REGION, L-CONSUMER
|
|
10
10
|
- `connector`: L-CONNECTOR-ENV
|
|
11
11
|
<!-- END GENERATED: library-uniform -->
|
|
12
12
|
|
|
@@ -108,6 +108,27 @@ try {
|
|
|
108
108
|
}
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
**It takes the RECORD above, not a bare error.** `handleError(error)` is not a
|
|
112
|
+
shorter spelling of `handleError({ error })`: the same three fields `logError()`
|
|
113
|
+
requires are required here too — `moduleName`, `operation` and `error` — because
|
|
114
|
+
`handleError()` logs before it classifies, through the same `UnifiedLogSchema`.
|
|
115
|
+
`context` is the only optional key.
|
|
116
|
+
|
|
117
|
+
A missing one is REFUSED, not defaulted, and the refusal comes from the schema by
|
|
118
|
+
the name of the field, checked in that order (`moduleName` → `operation` →
|
|
119
|
+
`error`):
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
[UnifiedLogSchema] Missing required field - "moduleName" must be a non-empty string
|
|
123
|
+
naming the module that failed; it is the caller's to provide and is never invented.
|
|
124
|
+
Fix: pass params.moduleName (ErrorHandlerCore takes it from errorData.moduleName).
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
So a caller that hands `handleError()` the caught error itself gets that message
|
|
128
|
+
about `moduleName` — which names the real defect, but only if the reader knows the
|
|
129
|
+
call takes a record. Measured by
|
|
130
|
+
`tests/unit/business-code-outside-the-dictionary.test.js` § the call contract.
|
|
131
|
+
|
|
111
132
|
### Execute with Retry
|
|
112
133
|
|
|
113
134
|
```javascript
|
|
@@ -167,6 +188,48 @@ same status carries both meanings and only the thrower knows which one it meant.
|
|
|
167
188
|
@see api/docs/governance/confirmations/workflow-failed-status-projection.md — 004
|
|
168
189
|
@see api/docs/biz/70-contracts/error-handling.md — the error classes
|
|
169
190
|
|
|
191
|
+
## Which codes the classifier answers for
|
|
192
|
+
|
|
193
|
+
`ErrorClassifier` reads three things and nothing else: this package's own
|
|
194
|
+
`ErrorTypes` (`error.type`), Node's network codes (`ECONNREFUSED`, `ETIMEDOUT`, …)
|
|
195
|
+
and **HTTP status codes** (`error.code` / `error.status`) — so `502`, `503` and
|
|
196
|
+
`504` carry their HTTP meaning here. AMQP reply codes share those numbers with a
|
|
197
|
+
different meaning, and they have a different owner: a publish failure is
|
|
198
|
+
classified inside the transport, before the error leaves it.
|
|
199
|
+
|
|
200
|
+
@see api/shared/mq-client-core/src/utils/publishErrors.js
|
|
201
|
+
|
|
202
|
+
Measured by `tests/unit/classified-domains-one-owner.test.js`: the table's numeric
|
|
203
|
+
keys are HTTP status codes, and the AMQP reply codes HTTP does not use (`406`,
|
|
204
|
+
`530`, `405`, …) are not keys of it.
|
|
205
|
+
|
|
206
|
+
### A business code this table does not know
|
|
207
|
+
|
|
208
|
+
**The classifier neither labels nor refuses it.** A business name it has never heard
|
|
209
|
+
of — `REFERENCE_RECORD_MISSING`, say — simply misses the `code` lookup, and the class
|
|
210
|
+
comes from the next thing the error declares: `error.type`, then `error.code` in the
|
|
211
|
+
table, then `error.status`. So an unknown name at `422` is `VALIDATION`, exactly like
|
|
212
|
+
a bare `422`; the same unknown name at `503` is `TRANSIENT`. Two different unknown
|
|
213
|
+
names with one status get one class, because **the name is not read at all**.
|
|
214
|
+
|
|
215
|
+
`retryable` is not read here either. It belongs to the error contract and is consumed
|
|
216
|
+
elsewhere (`ErrorMapper.CONTRACT_RETRYABLE_CODES`, the dispatcher's `_canRetry`); on
|
|
217
|
+
this classifier it changes nothing, and whether a retry happens follows from the
|
|
218
|
+
CLASS (`ErrorHandlerCore._decideAction`: `TRANSIENT`/`TIMEOUT`/`RATE_LIMIT` retry,
|
|
219
|
+
`BUSINESS`/`VALIDATION` do not).
|
|
220
|
+
|
|
221
|
+
This is a rule, not an omission: **the dictionary of business code NAMES is not this
|
|
222
|
+
package's to own.** It lives in `api/docs/biz/70-contracts/error-handling.md` §1
|
|
223
|
+
(BIZ-DOCS), and a new name is agreed through the process `error-code-dictionary` 002.
|
|
224
|
+
A name branch here would make the class depend on a dictionary this package cannot
|
|
225
|
+
see, which is two owners of one fact (`change-discipline.md` § One rail per concern).
|
|
226
|
+
Measured 2026-09-22 (d.803): nothing in the workspace marks a business code as
|
|
227
|
+
"unknown" — the only machine-readable code list is `CONTRACT_RETRYABLE_CODES`, which
|
|
228
|
+
is the retryable set and nothing else.
|
|
229
|
+
|
|
230
|
+
Measured by `tests/unit/business-code-outside-the-dictionary.test.js`, including the
|
|
231
|
+
control that the table really does not carry the name.
|
|
232
|
+
|
|
170
233
|
## API Reference
|
|
171
234
|
|
|
172
235
|
See [ERROR_HANDLING.md](/docs/standards/ERROR_HANDLING.md) for the cross-cutting standard (classification table, fail-loudly rules).
|
|
@@ -183,5 +246,5 @@ See [ERROR_HANDLING.md](/docs/standards/ERROR_HANDLING.md) for the cross-cutting
|
|
|
183
246
|
|
|
184
247
|
---
|
|
185
248
|
|
|
186
|
-
*
|
|
249
|
+
*License: MIT*
|
|
187
250
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/error-handler-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Core error handling functionality for OA Drive microservices - classification, retry, circuit breaker, DLQ routing",
|
|
5
5
|
"oa": {
|
|
6
6
|
"category": "connector"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"author": "OnlineApps",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@onlineapps/logger-contract": "2.
|
|
28
|
+
"@onlineapps/logger-contract": "2.1.0",
|
|
29
29
|
"@onlineapps/runtime-config": "1.2.0",
|
|
30
30
|
"opossum": "10.0.0"
|
|
31
31
|
},
|
package/src/ErrorClassifier.js
CHANGED
|
@@ -29,6 +29,24 @@ const ErrorCodes = {
|
|
|
29
29
|
ETIMEDOUT: ErrorTypes.TRANSIENT,
|
|
30
30
|
ECONNRESET: ErrorTypes.TRANSIENT,
|
|
31
31
|
EPIPE: ErrorTypes.TRANSIENT,
|
|
32
|
+
|
|
33
|
+
// The same family, measured on real sockets and missing from this table until d.774:
|
|
34
|
+
// `EAI_AGAIN` is the resolver saying "ask again" (node `dns`, `getaddrinfo`), and
|
|
35
|
+
// `ENETUNREACH` / `EHOSTUNREACH` are routing (node `net`). All three describe a peer
|
|
36
|
+
// that may be reachable on the next attempt, which is what TRANSIENT means here.
|
|
37
|
+
// Until d.774 they were not in the table at all, so they answered UNKNOWN and the
|
|
38
|
+
// delivery was NOT retried.
|
|
39
|
+
// Measured shapes: infra/api_delivery_dispatcher/tests/unit/transportErrorShapes.test.js
|
|
40
|
+
EAI_AGAIN: ErrorTypes.TRANSIENT,
|
|
41
|
+
ENETUNREACH: ErrorTypes.TRANSIENT,
|
|
42
|
+
EHOSTUNREACH: ErrorTypes.TRANSIENT,
|
|
43
|
+
|
|
44
|
+
// axios's own deadline (it aborts the request itself, so there is no `cause` and no
|
|
45
|
+
// syscall underneath) — and the most common real shape of a failed delivery. TIMEOUT
|
|
46
|
+
// is the category this table already has for "the deadline ran out"; UNKNOWN, which is
|
|
47
|
+
// what it answered until d.774, meant the message went to the DLQ without one retry.
|
|
48
|
+
ECONNABORTED: ErrorTypes.TIMEOUT,
|
|
49
|
+
|
|
32
50
|
|
|
33
51
|
// HTTP status codes
|
|
34
52
|
408: ErrorTypes.TIMEOUT,
|
|
@@ -40,8 +58,6 @@ const ErrorCodes = {
|
|
|
40
58
|
|
|
41
59
|
// Circuit breaker refusal (CircuitOpenError) -> TRANSIENT: the state heals
|
|
42
60
|
// itself once the breaker's resetTimeout elapses, so retrying is allowed.
|
|
43
|
-
// Pinned by code, never by message wording — the word "resetTimeout" in the
|
|
44
|
-
// fix hint would otherwise read as TIMEOUT via the patterns below.
|
|
45
61
|
// @see api/docs/governance/confirmations/circuit-open-classification.md
|
|
46
62
|
CIRCUIT_OPEN: ErrorTypes.TRANSIENT,
|
|
47
63
|
|
|
@@ -57,7 +73,25 @@ const ErrorCodes = {
|
|
|
57
73
|
class ErrorClassifier {
|
|
58
74
|
/**
|
|
59
75
|
* Classify error into type for appropriate handling
|
|
60
|
-
*
|
|
76
|
+
*
|
|
77
|
+
* A BUSINESS CODE THIS TABLE DOES NOT KNOW IS NEITHER LABELLED NOR REFUSED (d.804).
|
|
78
|
+
* A name it has never heard of — `REFERENCE_RECORD_MISSING`, say — misses the `code`
|
|
79
|
+
* lookup below and the class comes from the next thing the error declares: `type`,
|
|
80
|
+
* then `code`, then `status`. So an unknown name at `422` is `VALIDATION`, exactly
|
|
81
|
+
* like a bare `422`, and two different unknown names with one status get one class:
|
|
82
|
+
* the NAME is not read at all. `error.retryable` is not read here either — it belongs
|
|
83
|
+
* to the error contract and is consumed elsewhere
|
|
84
|
+
* (`ErrorMapper.CONTRACT_RETRYABLE_CODES`, the dispatcher's `_canRetry`).
|
|
85
|
+
*
|
|
86
|
+
* That is a rule, not a gap. The dictionary of business code NAMES is not this
|
|
87
|
+
* package's to own: it lives in `api/docs/biz/70-contracts/error-handling.md` §1
|
|
88
|
+
* (BIZ-DOCS, process `error-code-dictionary` 002). A name branch here would make the
|
|
89
|
+
* class depend on a dictionary this package cannot see — two owners of one fact
|
|
90
|
+
* (`change-discipline.md` § One rail per concern).
|
|
91
|
+
*
|
|
92
|
+
* @see ../README.md § A business code this table does not know
|
|
93
|
+
* @see ../tests/unit/business-code-outside-the-dictionary.test.js
|
|
94
|
+
*
|
|
61
95
|
* @param {Error} error - Error to classify
|
|
62
96
|
* @returns {string} Error type from ErrorTypes enum
|
|
63
97
|
*/
|
|
@@ -88,30 +122,18 @@ class ErrorClassifier {
|
|
|
88
122
|
return ErrorCodes[error.status];
|
|
89
123
|
}
|
|
90
124
|
|
|
91
|
-
// 4.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return ErrorTypes.VALIDATION;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (message.includes('fatal') || message.includes('critical')) {
|
|
107
|
-
return ErrorTypes.FATAL;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (message.includes('connection') || message.includes('network') || message.includes('econn')) {
|
|
111
|
-
return ErrorTypes.TRANSIENT;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 5. Default
|
|
125
|
+
// 4. Nothing declared it. The message is NOT consulted: it is a human
|
|
126
|
+
// string the error contract does not classify by, and reading it was a
|
|
127
|
+
// second rail beside the table above (`change-discipline.md` § One rail per
|
|
128
|
+
// concern). Removed in d.761 after measuring every live producer — none
|
|
129
|
+
// reached it: `DeliveryDispatcher._canRetry` classifies a `{ status, code }`
|
|
130
|
+
// with no message at all, the orchestrator sends `type`/`status`/`code` and
|
|
131
|
+
// routes a failed publish around this classifier entirely, and
|
|
132
|
+
// `conn-infra-error-handler` only delegates. The one shape it did answer
|
|
133
|
+
// for was a service's own internal exception, which it mis-filed: a
|
|
134
|
+
// `TypeError` saying "invalid" became VALIDATION, and with it the log
|
|
135
|
+
// severity `warn` — a defect of our code filed as a warning about input.
|
|
136
|
+
// An unclassified error is UNKNOWN, and UNKNOWN is not retried.
|
|
115
137
|
return ErrorTypes.UNKNOWN;
|
|
116
138
|
}
|
|
117
139
|
|