@constructive-io/errors
Advanced tools
| /** Status used when a code carries no mapping. */ | ||
| export declare const UNMAPPED_HTTP_STATUS = 500; | ||
| export interface HttpStatusResolution { | ||
| /** The status a transport should send. */ | ||
| status: number; | ||
| /** | ||
| * Whether `status` came from the registry. `false` means the code is not | ||
| * registered and `status` is the {@link UNMAPPED_HTTP_STATUS} fallback — the | ||
| * one case where a 500 does not mean "the server broke". | ||
| */ | ||
| mapped: boolean; | ||
| } | ||
| /** Notified once per unmapped code. */ | ||
| export type UnmappedStatusReporter = (code: string) => void; | ||
| /** | ||
| * Replace the unmapped-code reporter (pass `null` to restore the default, or a | ||
| * no-op to silence it). Transports with a real logger should route it there. | ||
| */ | ||
| export declare function setUnmappedStatusReporter(next: UnmappedStatusReporter | null): void; | ||
| /** Test seam: forget which codes have already been reported. */ | ||
| export declare function resetUnmappedStatusReports(): void; | ||
| /** | ||
| * Resolve the HTTP status for an error code. | ||
| * | ||
| * An unregistered code is reported (once per code) rather than silently | ||
| * degrading to 500: a refusal that is plainly a 403 or 409 turning into a 500 | ||
| * looks like a crash, and the only way anyone notices is by reading the | ||
| * transport's source. | ||
| */ | ||
| export declare function httpStatusFor(code: string | null | undefined): HttpStatusResolution; |
+39
| import { getDefinition } from './registry'; | ||
| /** Status used when a code carries no mapping. */ | ||
| export const UNMAPPED_HTTP_STATUS = 500; | ||
| const reported = new Set(); | ||
| const defaultReporter = code => { | ||
| // eslint-disable-next-line no-console | ||
| console.warn(`[constructive-errors] no HTTP status mapping for ${code}; responding ${UNMAPPED_HTTP_STATUS}. ` + | ||
| 'Register the code so intentional refusals stop surfacing as server errors.'); | ||
| }; | ||
| let reporter = defaultReporter; | ||
| /** | ||
| * Replace the unmapped-code reporter (pass `null` to restore the default, or a | ||
| * no-op to silence it). Transports with a real logger should route it there. | ||
| */ | ||
| export function setUnmappedStatusReporter(next) { | ||
| reporter = next ?? defaultReporter; | ||
| } | ||
| /** Test seam: forget which codes have already been reported. */ | ||
| export function resetUnmappedStatusReports() { | ||
| reported.clear(); | ||
| } | ||
| /** | ||
| * Resolve the HTTP status for an error code. | ||
| * | ||
| * An unregistered code is reported (once per code) rather than silently | ||
| * degrading to 500: a refusal that is plainly a 403 or 409 turning into a 500 | ||
| * looks like a crash, and the only way anyone notices is by reading the | ||
| * transport's source. | ||
| */ | ||
| export function httpStatusFor(code) { | ||
| const def = code ? getDefinition(code) : undefined; | ||
| if (def) | ||
| return { status: def.http, mapped: true }; | ||
| if (code && !reported.has(code)) { | ||
| reported.add(code); | ||
| reporter(code); | ||
| } | ||
| return { status: UNMAPPED_HTTP_STATUS, mapped: false }; | ||
| } |
+30
| /** Status used when a code carries no mapping. */ | ||
| export declare const UNMAPPED_HTTP_STATUS = 500; | ||
| export interface HttpStatusResolution { | ||
| /** The status a transport should send. */ | ||
| status: number; | ||
| /** | ||
| * Whether `status` came from the registry. `false` means the code is not | ||
| * registered and `status` is the {@link UNMAPPED_HTTP_STATUS} fallback — the | ||
| * one case where a 500 does not mean "the server broke". | ||
| */ | ||
| mapped: boolean; | ||
| } | ||
| /** Notified once per unmapped code. */ | ||
| export type UnmappedStatusReporter = (code: string) => void; | ||
| /** | ||
| * Replace the unmapped-code reporter (pass `null` to restore the default, or a | ||
| * no-op to silence it). Transports with a real logger should route it there. | ||
| */ | ||
| export declare function setUnmappedStatusReporter(next: UnmappedStatusReporter | null): void; | ||
| /** Test seam: forget which codes have already been reported. */ | ||
| export declare function resetUnmappedStatusReports(): void; | ||
| /** | ||
| * Resolve the HTTP status for an error code. | ||
| * | ||
| * An unregistered code is reported (once per code) rather than silently | ||
| * degrading to 500: a refusal that is plainly a 403 or 409 turning into a 500 | ||
| * looks like a crash, and the only way anyone notices is by reading the | ||
| * transport's source. | ||
| */ | ||
| export declare function httpStatusFor(code: string | null | undefined): HttpStatusResolution; |
+45
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.UNMAPPED_HTTP_STATUS = void 0; | ||
| exports.setUnmappedStatusReporter = setUnmappedStatusReporter; | ||
| exports.resetUnmappedStatusReports = resetUnmappedStatusReports; | ||
| exports.httpStatusFor = httpStatusFor; | ||
| const registry_1 = require("./registry"); | ||
| /** Status used when a code carries no mapping. */ | ||
| exports.UNMAPPED_HTTP_STATUS = 500; | ||
| const reported = new Set(); | ||
| const defaultReporter = code => { | ||
| // eslint-disable-next-line no-console | ||
| console.warn(`[constructive-errors] no HTTP status mapping for ${code}; responding ${exports.UNMAPPED_HTTP_STATUS}. ` + | ||
| 'Register the code so intentional refusals stop surfacing as server errors.'); | ||
| }; | ||
| let reporter = defaultReporter; | ||
| /** | ||
| * Replace the unmapped-code reporter (pass `null` to restore the default, or a | ||
| * no-op to silence it). Transports with a real logger should route it there. | ||
| */ | ||
| function setUnmappedStatusReporter(next) { | ||
| reporter = next ?? defaultReporter; | ||
| } | ||
| /** Test seam: forget which codes have already been reported. */ | ||
| function resetUnmappedStatusReports() { | ||
| reported.clear(); | ||
| } | ||
| /** | ||
| * Resolve the HTTP status for an error code. | ||
| * | ||
| * An unregistered code is reported (once per code) rather than silently | ||
| * degrading to 500: a refusal that is plainly a 403 or 409 turning into a 500 | ||
| * looks like a crash, and the only way anyone notices is by reading the | ||
| * transport's source. | ||
| */ | ||
| function httpStatusFor(code) { | ||
| const def = code ? (0, registry_1.getDefinition)(code) : undefined; | ||
| if (def) | ||
| return { status: def.http, mapped: true }; | ||
| if (code && !reported.has(code)) { | ||
| reported.add(code); | ||
| reporter(code); | ||
| } | ||
| return { status: exports.UNMAPPED_HTTP_STATUS, mapped: false }; | ||
| } |
| /** | ||
| * GENERATED FILE — DO NOT EDIT BY HAND. | ||
| * | ||
| * Source of truth: the constructive-db error audit (531 distinct codes | ||
| * Source of truth: the constructive-db error audit (546 distinct codes | ||
| * raised via EXCEPTION/THROW across deploy sources + generated output). | ||
@@ -12,3 +12,3 @@ * Regenerate with `python3 scripts/generate-registry.py` (see README.md). | ||
| * | ||
| * Counts: 531 total, 401 public, 130 internal. | ||
| * Counts: 546 total, 410 public, 136 internal. | ||
| */ | ||
@@ -28,2 +28,2 @@ import { type DefinedError } from '../define'; | ||
| /** Total number of codes collected from constructive-db. */ | ||
| export declare const GENERATED_CODE_COUNT = 531; | ||
| export declare const GENERATED_CODE_COUNT = 546; |
+1
-0
@@ -5,2 +5,3 @@ export * from './classify'; | ||
| export * from './format'; | ||
| export * from './http'; | ||
| export * from './interpolate'; | ||
@@ -7,0 +8,0 @@ export * from './parse'; |
+1
-0
@@ -5,2 +5,3 @@ export * from './classify'; | ||
| export * from './format'; | ||
| export * from './http'; | ||
| export * from './interpolate'; | ||
@@ -7,0 +8,0 @@ export * from './parse'; |
+3
-1
@@ -28,4 +28,6 @@ import { ConstructiveError } from './error'; | ||
| * error's raw message when the code is unknown) and the registry's HTTP hint. | ||
| * Codes that could not be resolved become `UNKNOWN_ERROR` (internal). | ||
| * Codes that could not be resolved become `UNKNOWN_ERROR` (internal); a code | ||
| * with no registered status is reported by {@link httpStatusFor} rather than | ||
| * quietly becoming a 500. | ||
| */ | ||
| export declare function toError(error: unknown, locale?: string): ConstructiveError; |
+5
-2
| import { classify } from './classify'; | ||
| import { ConstructiveError } from './error'; | ||
| import { format } from './format'; | ||
| import { httpStatusFor } from './http'; | ||
| import { extractPgErrorFields, RAISE_EXCEPTION_SQLSTATE, SQLSTATE_TO_CODE } from './pg'; | ||
@@ -181,3 +182,5 @@ import { getDefinition } from './registry'; | ||
| * error's raw message when the code is unknown) and the registry's HTTP hint. | ||
| * Codes that could not be resolved become `UNKNOWN_ERROR` (internal). | ||
| * Codes that could not be resolved become `UNKNOWN_ERROR` (internal); a code | ||
| * with no registered status is reported by {@link httpStatusFor} rather than | ||
| * quietly becoming a 500. | ||
| */ | ||
@@ -197,5 +200,5 @@ export function toError(error, locale) { | ||
| errorClass: parsed.class, | ||
| http: def?.http ?? 500, | ||
| http: def ? def.http : httpStatusFor(code).status, | ||
| context: parsed.context | ||
| }); | ||
| } |
| /** | ||
| * GENERATED FILE — DO NOT EDIT BY HAND. | ||
| * | ||
| * Source of truth: the constructive-db error audit (531 distinct codes | ||
| * Source of truth: the constructive-db error audit (546 distinct codes | ||
| * raised via EXCEPTION/THROW across deploy sources + generated output). | ||
@@ -12,3 +12,3 @@ * Regenerate with `python3 scripts/generate-registry.py` (see README.md). | ||
| * | ||
| * Counts: 531 total, 401 public, 130 internal. | ||
| * Counts: 546 total, 410 public, 136 internal. | ||
| */ | ||
@@ -28,2 +28,2 @@ import { type DefinedError } from '../define'; | ||
| /** Total number of codes collected from constructive-db. */ | ||
| export declare const GENERATED_CODE_COUNT = 531; | ||
| export declare const GENERATED_CODE_COUNT = 546; |
+1
-0
@@ -5,2 +5,3 @@ export * from './classify'; | ||
| export * from './format'; | ||
| export * from './http'; | ||
| export * from './interpolate'; | ||
@@ -7,0 +8,0 @@ export * from './parse'; |
+1
-0
@@ -21,2 +21,3 @@ "use strict"; | ||
| __exportStar(require("./format"), exports); | ||
| __exportStar(require("./http"), exports); | ||
| __exportStar(require("./interpolate"), exports); | ||
@@ -23,0 +24,0 @@ __exportStar(require("./parse"), exports); |
+2
-2
| { | ||
| "name": "@constructive-io/errors", | ||
| "version": "0.9.0", | ||
| "version": "0.10.0", | ||
| "author": "Constructive <developers@constructive.io>", | ||
@@ -44,3 +44,3 @@ "description": "Canonical Constructive error system: registry, codes, i18n messages, and cross-source (PostgreSQL/GraphQL/client) parsing. Zero runtime dependencies.", | ||
| }, | ||
| "gitHead": "d508453a32a2f1d70ff6916871046615226e6f33" | ||
| "gitHead": "43121004a8bedd2c5fcf7bf33208079d55bad01f" | ||
| } |
+3
-1
@@ -28,4 +28,6 @@ import { ConstructiveError } from './error'; | ||
| * error's raw message when the code is unknown) and the registry's HTTP hint. | ||
| * Codes that could not be resolved become `UNKNOWN_ERROR` (internal). | ||
| * Codes that could not be resolved become `UNKNOWN_ERROR` (internal); a code | ||
| * with no registered status is reported by {@link httpStatusFor} rather than | ||
| * quietly becoming a 500. | ||
| */ | ||
| export declare function toError(error: unknown, locale?: string): ConstructiveError; |
+5
-2
@@ -8,2 +8,3 @@ "use strict"; | ||
| const format_1 = require("./format"); | ||
| const http_1 = require("./http"); | ||
| const pg_1 = require("./pg"); | ||
@@ -186,3 +187,5 @@ const registry_1 = require("./registry"); | ||
| * error's raw message when the code is unknown) and the registry's HTTP hint. | ||
| * Codes that could not be resolved become `UNKNOWN_ERROR` (internal). | ||
| * Codes that could not be resolved become `UNKNOWN_ERROR` (internal); a code | ||
| * with no registered status is reported by {@link httpStatusFor} rather than | ||
| * quietly becoming a 500. | ||
| */ | ||
@@ -202,5 +205,5 @@ function toError(error, locale) { | ||
| errorClass: parsed.class, | ||
| http: def?.http ?? 500, | ||
| http: def ? def.http : (0, http_1.httpStatusFor)(code).status, | ||
| context: parsed.context | ||
| }); | ||
| } |
+29
-0
@@ -18,2 +18,5 @@ # @constructive-io/errors | ||
| (fail safe) so transports never leak unregistered errors. | ||
| - **`httpStatusFor(code)`** — the canonical code → HTTP status mapping, so no | ||
| transport keeps its own table. Unregistered codes answer `500` *and are | ||
| reported*, never silently. | ||
@@ -50,2 +53,28 @@ ```ts | ||
| ## HTTP status | ||
| Every registry entry carries `http`, so an HTTP surface never needs its own | ||
| code → status table: `toError(err).http`, or `httpStatusFor(code)` when all you | ||
| have is a code. | ||
| ```ts | ||
| import { httpStatusFor, setUnmappedStatusReporter, toError } from '@constructive-io/errors'; | ||
| setUnmappedStatusReporter(code => log.warn({ code }, 'error code has no HTTP status')); | ||
| const err = toError(caught); | ||
| res.status(err.http).json(err.toExtensions()); | ||
| httpStatusFor('ACCOUNT_DISABLED'); // { status: 403, mapped: true } | ||
| httpStatusFor('BRAND_NEW_CODE'); // { status: 500, mapped: false } + one report | ||
| ``` | ||
| `mapped: false` is the one case where a 500 does not mean "the server broke" — | ||
| it means the code never reached the registry. That is the failure mode this | ||
| exists to make loud: a refusal that is plainly a 403 or a 409 answering 500 | ||
| looks like a crash, and the codes most likely to be missing are the newest ones. | ||
| When a constructive-db release adds codes, refresh the registry (below); | ||
| `__tests__/registry-sync.test.ts` fails if the snapshot and the generated | ||
| registry disagree. | ||
| ## Regenerating the full registry | ||
@@ -52,0 +81,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
439863
3.38%55
7.84%5171
4.4%174
20%