@onlineapps/error-handler-core 3.0.0 → 4.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/CHANGELOG.md +95 -0
- package/README.md +80 -1
- package/package.json +1 -1
- package/src/ErrorClassifier.js +56 -27
- package/src/ErrorHandlerCore.js +8 -0
- package/src/UnifiedLogSchema.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,101 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [4.0.0] — 2026-09-27
|
|
8
|
+
|
|
9
|
+
### Changed — BREAKING — `ErrorClassifier.classify()` už nečte `error.message`: třídu dává jen `type`, `code` a `status` (d.761)
|
|
10
|
+
|
|
11
|
+
Vydaná 3.1.0 měla ve `classify()` čtvrtý krok, který po `type`, `code` a `status`
|
|
12
|
+
hledal v `error.message` (převedeném na malá písmena) podřetězce: `timeout` →
|
|
13
|
+
`TIMEOUT`; `rate limit` / `rate_limit` → `RATE_LIMIT`; `validation` / `invalid` →
|
|
14
|
+
`VALIDATION`; `fatal` / `critical` → `FATAL`; `connection` / `network` / `econn` →
|
|
15
|
+
`TRANSIENT`. Ten krok je pryč. Chyba, kterou nedeklaruje žádné z těch tří polí, je
|
|
16
|
+
`UNKNOWN` bez ohledu na znění — a `UNKNOWN` se neopakuje a loguje se se závažností
|
|
17
|
+
`error`.
|
|
18
|
+
|
|
19
|
+
Důvod: hláška je lidský řetězec, podle kterého kontrakt chyby neklasifikuje, a čtení
|
|
20
|
+
znění byla druhá kolej vedle tabulky kódů (`change-discipline.md` § One rail per
|
|
21
|
+
concern) — každé přeformulování hlášky tiše měnilo třídu. Měření živých producentů
|
|
22
|
+
nenašlo nikoho, kdo by na krok dosáhl (`DeliveryDispatcher._canRetry` skládá
|
|
23
|
+
`{ status, code }` bez hlášky, orchestrátor posílá `type`/`status`/`code`,
|
|
24
|
+
`conn-infra-error-handler` jen deleguje). Jediný zásah byla vnitřní výjimka služby:
|
|
25
|
+
`TypeError` se slovem „invalid" se zařadil jako `VALIDATION`, a tím dostal závažnost
|
|
26
|
+
`warn` — vada našeho kódu podaná jako varování o vstupu. Teď je `UNKNOWN`, tedy
|
|
27
|
+
`error`.
|
|
28
|
+
|
|
29
|
+
Migrace: volající, který chce konkrétní třídu, ji deklaruje strojově — `error.type`
|
|
30
|
+
(hodnota z `ErrorTypes`), `error.code` z tabulky `ErrorCodes`, nebo `error.status`.
|
|
31
|
+
Kontrolní případy drží: `CIRCUIT_OPEN` → `TRANSIENT`, `422` → `VALIDATION`,
|
|
32
|
+
`503` → `TRANSIENT`.
|
|
33
|
+
|
|
34
|
+
### Fixed — čtyři změřené tvary chyb přenosu mají kategorii (d.774)
|
|
35
|
+
|
|
36
|
+
V tabulce `ErrorCodes` chyběly, takže odpovídaly `UNKNOWN` — a `UNKNOWN` se
|
|
37
|
+
neopakuje, doručení tedy skončilo bez jediného dalšího pokusu:
|
|
38
|
+
|
|
39
|
+
- `EAI_AGAIN` (resolver říká „zeptej se znovu", `getaddrinfo`), `ENETUNREACH`,
|
|
40
|
+
`EHOSTUNREACH` (směrování) → `TRANSIENT` — tatáž rodina jako `ECONNREFUSED`:
|
|
41
|
+
protistrana tam příště být může;
|
|
42
|
+
- `ECONNABORTED` (vypršená lhůta axiosu, který požadavek přeruší sám, proto bez
|
|
43
|
+
`cause` a bez syscallu) → `TIMEOUT`.
|
|
44
|
+
|
|
45
|
+
Tvary jsou změřené na skutečných socketech
|
|
46
|
+
(`infra/api_delivery_dispatcher/tests/unit/transportErrorShapes.test.js`). Nic jiného
|
|
47
|
+
se v tabulce nehnulo: `ECONNREFUSED`, `ENOTFOUND`, `ETIMEDOUT`, `ECONNRESET`, `EPIPE`
|
|
48
|
+
a `CIRCUIT_OPEN` drží svou třídu; trvalé tvary (`EPROTO`, `ERR_INVALID_URL`,
|
|
49
|
+
`ERR_BAD_REQUEST`, certifikátové) a `ERR_CANCELED` zůstávají `UNKNOWN`.
|
|
50
|
+
|
|
51
|
+
### Documentation — neznámý business kód se neoznačuje ani neodmítá (d.804)
|
|
52
|
+
|
|
53
|
+
README (§ A business code this table does not know) a JSDoc `classify()` říkají
|
|
54
|
+
pravidlo, které kód už měl: jméno business kódu, které tabulka nezná (např.
|
|
55
|
+
`REFERENCE_RECORD_MISSING`), mine vyhledání podle `code` a třídu dá další deklarované
|
|
56
|
+
pole — `error.type`, `error.code` v tabulce, `error.status`. Neznámé jméno na `422` je
|
|
57
|
+
tedy `VALIDATION` jako holé `422`, na `503` `TRANSIENT`; `error.retryable` klasifikátor
|
|
58
|
+
nečte. Slovník jmen business kódů tomuto balíčku nepatří — vlastní ho
|
|
59
|
+
`api/docs/biz/70-contracts/error-handling.md` §1.
|
|
60
|
+
|
|
61
|
+
### Documentation — `handleError()` bere záznam, ne holou chybu (d.804b)
|
|
62
|
+
|
|
63
|
+
README říká, že `handleError()` vyžaduje tatáž tři pole jako `logError()` —
|
|
64
|
+
`moduleName`, `operation` a `error` (`context` je jediný nepovinný klíč) — a že chybějící
|
|
65
|
+
pole schéma odmítne jménem, v pořadí `moduleName` → `operation` → `error`. Volající,
|
|
66
|
+
který předá zachycenou chybu samotnou, dostane doslova:
|
|
67
|
+
`[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).`
|
|
68
|
+
|
|
69
|
+
### Documentation — tabulka kódů je doména HTTP a Node, verdikt AMQP má jiného vlastníka (d.718)
|
|
70
|
+
|
|
71
|
+
README (§ Which codes the classifier answers for): `ErrorClassifier` čte jen vlastní
|
|
72
|
+
`ErrorTypes` (`error.type`), síťové kódy Node (`ECONNREFUSED`, `ETIMEDOUT`, …) a stavové
|
|
73
|
+
kódy HTTP (`error.code` / `error.status`) — `502`, `503` a `504` tu mají význam HTTP.
|
|
74
|
+
Selhání publikace klasifikuje transport, než chyba opustí
|
|
75
|
+
`@onlineapps/mq-client-core` (`src/utils/publishErrors.js`).
|
|
76
|
+
|
|
77
|
+
### Documentation — README nepíše verzi balíčku rukou (d.730)
|
|
78
|
+
|
|
79
|
+
Patička README tvrdila `*Version: 1.0.0 | License: MIT*`; verzi vlastní
|
|
80
|
+
`package.json`, patička je teď `*License: MIT*`.
|
|
81
|
+
|
|
82
|
+
## [3.1.0] — 2026-09-25
|
|
83
|
+
|
|
84
|
+
### Added — třída `ErrorTypes.DECLINED`: pravidlo práci zakázalo (d.831)
|
|
85
|
+
|
|
86
|
+
Každá dosavadní třída pojmenovává způsob, jakým se práce NEPOVEDLA. `DECLINED` říká,
|
|
87
|
+
že se dělat neměla: doklad už odeslaný byl, položku zdroj neprojednává. Je koncová,
|
|
88
|
+
není to selhání, neopakuje se a nejde do DLQ — `ErrorHandlerCore` pro ni vrací verdikt
|
|
89
|
+
`declined`, který je (stejně jako `dlq`) klasifikací pro volajícího, ne cestou, kterou
|
|
90
|
+
by tenhle balíček sám šel.
|
|
91
|
+
|
|
92
|
+
Deklaruje ji VYHAZUJÍCÍ přes `error.type`; ze stavového kódu se neodvozuje. Tabulka
|
|
93
|
+
kódů zůstává beze změny — každé jiné `409` je dál `BUSINESS`, protože týž stav nese
|
|
94
|
+
oba významy a jen vyhazující ví, který z nich myslel.
|
|
95
|
+
|
|
96
|
+
Rozhodnutí vlastníka:
|
|
97
|
+
`api/docs/governance/confirmations/workflow-failed-status-projection.md` 004
|
|
98
|
+
(odmítnutí bránou komba není selhání běhu). Závažnost logu `info`, protože to není
|
|
99
|
+
incident; mapa kategorií v `UnifiedLogSchema` ji kontroluje při načtení, takže třída
|
|
100
|
+
bez závažnosti balíček neprojde.
|
|
101
|
+
|
|
7
102
|
## [3.0.0] — 2026-09-16
|
|
8
103
|
|
|
9
104
|
### Removed — **BREAKING** `DLQRouter`: dead-lettering má jednu kolej, a není v tomto balíčku
|
package/README.md
CHANGED
|
@@ -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
|
|
@@ -151,6 +172,64 @@ Branch on `error.code`, never on `error.message`: the wording follows
|
|
|
151
172
|
`.claude/rules/architecture-principles.md` §5 and may be improved, the code and
|
|
152
173
|
`details.name` are the contract.
|
|
153
174
|
|
|
175
|
+
## `DECLINED` — a rule forbade the work
|
|
176
|
+
|
|
177
|
+
Every class in `ErrorTypes` names a way the work FAILED, except this one. `DECLINED`
|
|
178
|
+
says the work was refused: the domain rule that was asked says it must not happen —
|
|
179
|
+
a document already sent, an item the source will not discuss. It is terminal, it is
|
|
180
|
+
not a failure, it is not retried and it is not dead-lettered; `ErrorHandlerCore`
|
|
181
|
+
answers it with the verdict `declined`, which — like `dlq` — is a classification
|
|
182
|
+
returned to the caller, not a route this package takes.
|
|
183
|
+
|
|
184
|
+
It is declared by the THROWER, through `error.type`, and never derived from a status:
|
|
185
|
+
every other `409` stays `BUSINESS`, table and all. That is deliberate, because the
|
|
186
|
+
same status carries both meanings and only the thrower knows which one it meant.
|
|
187
|
+
|
|
188
|
+
@see api/docs/governance/confirmations/workflow-failed-status-projection.md — 004
|
|
189
|
+
@see api/docs/biz/70-contracts/error-handling.md — the error classes
|
|
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
|
+
|
|
154
233
|
## API Reference
|
|
155
234
|
|
|
156
235
|
See [ERROR_HANDLING.md](/docs/standards/ERROR_HANDLING.md) for the cross-cutting standard (classification table, fail-loudly rules).
|
|
@@ -167,5 +246,5 @@ See [ERROR_HANDLING.md](/docs/standards/ERROR_HANDLING.md) for the cross-cutting
|
|
|
167
246
|
|
|
168
247
|
---
|
|
169
248
|
|
|
170
|
-
*
|
|
249
|
+
*License: MIT*
|
|
171
250
|
|
package/package.json
CHANGED
package/src/ErrorClassifier.js
CHANGED
|
@@ -11,6 +11,13 @@ const ErrorTypes = {
|
|
|
11
11
|
VALIDATION: 'VALIDATION', // Input validation - DLQ
|
|
12
12
|
TIMEOUT: 'TIMEOUT', // Operation timeout - RETRY
|
|
13
13
|
RATE_LIMIT: 'RATE_LIMIT', // Rate limiting - RETRY with backoff
|
|
14
|
+
// A rule forbade the work. TERMINAL and NOT a failure: the caller asked for
|
|
15
|
+
// something the domain says must not happen — a document already sent, an
|
|
16
|
+
// item the source will not discuss — so there is nothing to retry and nothing
|
|
17
|
+
// to dead-letter. It is declared by the thrower through `error.type`, never
|
|
18
|
+
// derived from a status: every other 409 stays BUSINESS.
|
|
19
|
+
// @see api/docs/governance/confirmations/workflow-failed-status-projection.md 004
|
|
20
|
+
DECLINED: 'DECLINED', // Refused by a rule - TERMINAL, no retry, no DLQ
|
|
14
21
|
UNKNOWN: 'UNKNOWN' // Unclassified - LOG and THROW
|
|
15
22
|
};
|
|
16
23
|
|
|
@@ -22,6 +29,24 @@ const ErrorCodes = {
|
|
|
22
29
|
ETIMEDOUT: ErrorTypes.TRANSIENT,
|
|
23
30
|
ECONNRESET: ErrorTypes.TRANSIENT,
|
|
24
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
|
+
|
|
25
50
|
|
|
26
51
|
// HTTP status codes
|
|
27
52
|
408: ErrorTypes.TIMEOUT,
|
|
@@ -33,8 +58,6 @@ const ErrorCodes = {
|
|
|
33
58
|
|
|
34
59
|
// Circuit breaker refusal (CircuitOpenError) -> TRANSIENT: the state heals
|
|
35
60
|
// itself once the breaker's resetTimeout elapses, so retrying is allowed.
|
|
36
|
-
// Pinned by code, never by message wording — the word "resetTimeout" in the
|
|
37
|
-
// fix hint would otherwise read as TIMEOUT via the patterns below.
|
|
38
61
|
// @see api/docs/governance/confirmations/circuit-open-classification.md
|
|
39
62
|
CIRCUIT_OPEN: ErrorTypes.TRANSIENT,
|
|
40
63
|
|
|
@@ -50,7 +73,25 @@ const ErrorCodes = {
|
|
|
50
73
|
class ErrorClassifier {
|
|
51
74
|
/**
|
|
52
75
|
* Classify error into type for appropriate handling
|
|
53
|
-
*
|
|
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
|
+
*
|
|
54
95
|
* @param {Error} error - Error to classify
|
|
55
96
|
* @returns {string} Error type from ErrorTypes enum
|
|
56
97
|
*/
|
|
@@ -81,30 +122,18 @@ class ErrorClassifier {
|
|
|
81
122
|
return ErrorCodes[error.status];
|
|
82
123
|
}
|
|
83
124
|
|
|
84
|
-
// 4.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return ErrorTypes.VALIDATION;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (message.includes('fatal') || message.includes('critical')) {
|
|
100
|
-
return ErrorTypes.FATAL;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (message.includes('connection') || message.includes('network') || message.includes('econn')) {
|
|
104
|
-
return ErrorTypes.TRANSIENT;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 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.
|
|
108
137
|
return ErrorTypes.UNKNOWN;
|
|
109
138
|
}
|
|
110
139
|
|
package/src/ErrorHandlerCore.js
CHANGED
|
@@ -316,6 +316,14 @@ class ErrorHandlerCore {
|
|
|
316
316
|
case ErrorTypes.FATAL:
|
|
317
317
|
// Fatal errors, throw immediately
|
|
318
318
|
return { type: 'throw' };
|
|
319
|
+
|
|
320
|
+
case ErrorTypes.DECLINED:
|
|
321
|
+
// A rule forbade the work: terminal, and neither a retry nor a failure.
|
|
322
|
+
// Its own verdict rather than `dlq`, because dead-lettering says "look at
|
|
323
|
+
// this", and there is nothing to look at — the refusal IS the outcome
|
|
324
|
+
// (conf `workflow-failed-status-projection` 004). Like `dlq`, this is a
|
|
325
|
+
// CLASSIFICATION returned to the caller; this class routes nothing.
|
|
326
|
+
return { type: 'declined' };
|
|
319
327
|
|
|
320
328
|
default:
|
|
321
329
|
// Unknown errors, log and throw
|
package/src/UnifiedLogSchema.js
CHANGED
|
@@ -25,6 +25,11 @@ const SEVERITY_BY_TYPE = {
|
|
|
25
25
|
[ErrorTypes.VALIDATION]: 'warn',
|
|
26
26
|
[ErrorTypes.TIMEOUT]: 'error',
|
|
27
27
|
[ErrorTypes.RATE_LIMIT]: 'warn',
|
|
28
|
+
// Not an incident: a rule said no, and the run ended on that. `info` is the
|
|
29
|
+
// severity of a thing that happened as designed — filing it as `error` would
|
|
30
|
+
// put every refused send into the operator's error stream, which is the noise
|
|
31
|
+
// conf `workflow-failed-status-projection` 004 exists to remove.
|
|
32
|
+
[ErrorTypes.DECLINED]: 'info',
|
|
28
33
|
[ErrorTypes.UNKNOWN]: 'error'
|
|
29
34
|
};
|
|
30
35
|
|