@nextrush/errors 3.0.7 → 4.0.0-beta.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/README.md +321 -522
- package/dist/index.d.ts +123 -38
- package/dist/index.js +229 -19
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/audit-fixes.test.ts +181 -0
- package/src/__tests__/base.test.ts +2 -2
- package/src/__tests__/factory.test.ts +9 -2
- package/src/__tests__/header-validation.test.ts +43 -0
- package/src/__tests__/middleware.test.ts +120 -33
- package/src/__tests__/public-surface.test.ts +121 -0
- package/src/__tests__/security-audit-contribution.test.ts +40 -0
- package/src/base.ts +148 -3
- package/src/codes.ts +77 -0
- package/src/factory.ts +58 -1
- package/src/header-validation.ts +24 -0
- package/src/http-errors.ts +6 -0
- package/src/index.ts +7 -8
- package/src/middleware.ts +71 -43
- package/src/validation.ts +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Middleware
|
|
1
|
+
import { Context, Middleware } from '@nextrush/types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @nextrush/errors - Base Error Classes
|
|
@@ -7,6 +7,15 @@ import { Context, Middleware, Next } from '@nextrush/types';
|
|
|
7
7
|
*
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
|
+
/** Options accepted by {@link NextRushError.hydrate} when reconstructing errors. */
|
|
11
|
+
interface HydrateOptions {
|
|
12
|
+
status?: number;
|
|
13
|
+
code?: string;
|
|
14
|
+
details?: Record<string, unknown>;
|
|
15
|
+
requestId?: string;
|
|
16
|
+
traceId?: string;
|
|
17
|
+
timestamp?: string;
|
|
18
|
+
}
|
|
10
19
|
/**
|
|
11
20
|
* Base error class for all NextRush errors
|
|
12
21
|
*/
|
|
@@ -21,17 +30,45 @@ declare class NextRushError extends Error {
|
|
|
21
30
|
readonly details?: Record<string, unknown>;
|
|
22
31
|
/** Original error that caused this error */
|
|
23
32
|
readonly cause?: unknown;
|
|
33
|
+
/** Correlation/request identifier for tracing across boundaries (audit E-5). */
|
|
34
|
+
readonly requestId?: string;
|
|
35
|
+
/** Distributed-trace identifier (audit E-5). */
|
|
36
|
+
readonly traceId?: string;
|
|
37
|
+
/** ISO-8601 timestamp of when the error was created, when supplied. */
|
|
38
|
+
readonly timestamp?: string;
|
|
24
39
|
constructor(message: string, options?: {
|
|
25
40
|
status?: number;
|
|
26
41
|
code?: string;
|
|
27
42
|
expose?: boolean;
|
|
28
43
|
details?: Record<string, unknown>;
|
|
29
44
|
cause?: unknown;
|
|
45
|
+
requestId?: string;
|
|
46
|
+
traceId?: string;
|
|
47
|
+
timestamp?: string;
|
|
30
48
|
});
|
|
31
49
|
/**
|
|
32
50
|
* Convert error to JSON representation
|
|
33
51
|
*/
|
|
34
52
|
toJSON(): Record<string, unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Reconstruct a {@link NextRushError} from a serialized {@link toJSON} payload
|
|
55
|
+
* (audit E-7).
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* Enables cross-service transport: a downstream service can rebuild a typed
|
|
59
|
+
* error (with a working `instanceof NextRushError`) from the JSON it received,
|
|
60
|
+
* rather than being left with an opaque plain object. `message` falls back to
|
|
61
|
+
* a generic string when the source error was not exposed.
|
|
62
|
+
*
|
|
63
|
+
* @param json - A payload previously produced by {@link toJSON}.
|
|
64
|
+
* @returns A reconstructed {@link NextRushError}.
|
|
65
|
+
*/
|
|
66
|
+
static fromJSON(json: Record<string, unknown>): NextRushError;
|
|
67
|
+
/**
|
|
68
|
+
* Shared hydration logic for {@link fromJSON} across the hierarchy.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
protected static hydrate<T extends NextRushError>(json: Record<string, unknown>, build: (message: string, options: HydrateOptions) => T): T;
|
|
35
72
|
/**
|
|
36
73
|
* Create a response-safe version of the error
|
|
37
74
|
*/
|
|
@@ -53,9 +90,73 @@ declare class HttpError extends NextRushError {
|
|
|
53
90
|
expose?: boolean;
|
|
54
91
|
details?: Record<string, unknown>;
|
|
55
92
|
cause?: unknown;
|
|
93
|
+
requestId?: string;
|
|
94
|
+
traceId?: string;
|
|
95
|
+
timestamp?: string;
|
|
56
96
|
});
|
|
97
|
+
/**
|
|
98
|
+
* Reconstruct an {@link HttpError} from a serialized {@link NextRushError.toJSON}
|
|
99
|
+
* payload (audit E-7).
|
|
100
|
+
*
|
|
101
|
+
* @param json - A payload previously produced by `toJSON()`.
|
|
102
|
+
* @returns A reconstructed {@link HttpError}.
|
|
103
|
+
*/
|
|
104
|
+
static fromJSON(json: Record<string, unknown>): HttpError;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @nextrush/errors - Header Validation Error
|
|
109
|
+
*
|
|
110
|
+
* @packageDocumentation
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Thrown when a header field name or value fails RFC 9110 grammar
|
|
115
|
+
* validation (field-name token grammar, field-value grammar — no control
|
|
116
|
+
* characters, no leading/trailing whitespace, no obs-fold).
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* A rejected write here means the application (or a framework internal) is
|
|
120
|
+
* constructing an invalid header, not that a client sent bad input — so this
|
|
121
|
+
* is a 500-class programming error, not a validation-issue-list shape like
|
|
122
|
+
* {@link ValidationError}.
|
|
123
|
+
*/
|
|
124
|
+
declare class HeaderValidationError extends NextRushError {
|
|
125
|
+
constructor(message: string);
|
|
57
126
|
}
|
|
58
127
|
|
|
128
|
+
/**
|
|
129
|
+
* @nextrush/errors - Central Error Code Registry
|
|
130
|
+
*
|
|
131
|
+
* Single source of truth mapping HTTP status codes to their canonical,
|
|
132
|
+
* machine-readable error codes (audit E-4). Both the typed error classes and
|
|
133
|
+
* the {@link createError} factory resolve codes through this registry, so a
|
|
134
|
+
* given status maps to exactly one code regardless of construction path.
|
|
135
|
+
*
|
|
136
|
+
* @packageDocumentation
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* Canonical error code for each supported HTTP status.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* Keep this in sync with the typed classes in `http-errors.ts`. The
|
|
143
|
+
* `audit-fixes` test suite asserts `createError(status).code === ERROR_CODES[status]`
|
|
144
|
+
* for every entry, so drift between a class and this registry fails CI.
|
|
145
|
+
*/
|
|
146
|
+
declare const ERROR_CODES: Readonly<Record<number, string>>;
|
|
147
|
+
/**
|
|
148
|
+
* Resolve the canonical error code for an HTTP status.
|
|
149
|
+
*
|
|
150
|
+
* @param status - HTTP status code.
|
|
151
|
+
* @returns The registered canonical code, or `HTTP_<status>` for statuses with
|
|
152
|
+
* no dedicated class.
|
|
153
|
+
*/
|
|
154
|
+
declare function codeForStatus(status: number): string;
|
|
155
|
+
/** Generic internal-error code used when no status-specific code applies. */
|
|
156
|
+
declare const GENERIC_ERROR_CODE = "INTERNAL_ERROR";
|
|
157
|
+
/** Canonical code for validation failures. */
|
|
158
|
+
declare const VALIDATION_ERROR_CODE = "VALIDATION_ERROR";
|
|
159
|
+
|
|
59
160
|
/**
|
|
60
161
|
* @nextrush/errors - HTTP Error Classes
|
|
61
162
|
*
|
|
@@ -72,6 +173,12 @@ interface HttpErrorOptions {
|
|
|
72
173
|
expose?: boolean;
|
|
73
174
|
details?: Record<string, unknown>;
|
|
74
175
|
cause?: unknown;
|
|
176
|
+
/** Correlation/request identifier (audit E-5). */
|
|
177
|
+
requestId?: string;
|
|
178
|
+
/** Distributed-trace identifier (audit E-5). */
|
|
179
|
+
traceId?: string;
|
|
180
|
+
/** ISO-8601 creation timestamp (audit E-5). */
|
|
181
|
+
timestamp?: string;
|
|
75
182
|
}
|
|
76
183
|
/**
|
|
77
184
|
* 400 Bad Request - Invalid syntax or malformed request
|
|
@@ -505,31 +612,26 @@ declare function getSafeErrorMessage(error: unknown): string;
|
|
|
505
612
|
* @packageDocumentation
|
|
506
613
|
*/
|
|
507
614
|
|
|
508
|
-
/**
|
|
509
|
-
* Minimal context interface for error handling.
|
|
510
|
-
*
|
|
511
|
-
* @deprecated Use `Context` from `@nextrush/types` instead.
|
|
512
|
-
* Kept for backward compatibility — will be removed in v4.
|
|
513
|
-
*/
|
|
514
|
-
interface ErrorContext {
|
|
515
|
-
method: string;
|
|
516
|
-
path: string;
|
|
517
|
-
status: number;
|
|
518
|
-
json: (data: unknown) => void;
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Error handler middleware function type.
|
|
522
|
-
*
|
|
523
|
-
* @deprecated Use `Middleware` from `@nextrush/types` instead.
|
|
524
|
-
* Kept for backward compatibility — will be removed in v4.
|
|
525
|
-
*/
|
|
526
|
-
type ErrorMiddleware = Middleware;
|
|
527
615
|
/**
|
|
528
616
|
* Error handler options
|
|
529
617
|
*/
|
|
530
618
|
interface ErrorHandlerOptions {
|
|
531
619
|
/** Include stack trace in development */
|
|
532
620
|
includeStack?: boolean;
|
|
621
|
+
/**
|
|
622
|
+
* Whether the application is running in production (SEC-14).
|
|
623
|
+
*
|
|
624
|
+
* @remarks
|
|
625
|
+
* `@nextrush/errors` has no access to `app.isProduction` on its own — the
|
|
626
|
+
* caller threads it through explicitly, mirroring `@nextrush/core`'s
|
|
627
|
+
* `writeDefaultErrorResponse(opts.isProduction)`. When `true`, a truthy
|
|
628
|
+
* `includeStack` is ignored (fail closed) and a single warning is logged
|
|
629
|
+
* once per process, rather than once per request. Defaults to `false` so
|
|
630
|
+
* existing callers that don't pass this option keep today's behavior.
|
|
631
|
+
*
|
|
632
|
+
* @default false
|
|
633
|
+
*/
|
|
634
|
+
isProduction?: boolean;
|
|
533
635
|
/** Custom error logger */
|
|
534
636
|
logger?: (error: Error, ctx: Context) => void;
|
|
535
637
|
/** Custom error transformer */
|
|
@@ -562,22 +664,5 @@ declare function errorHandler(options?: ErrorHandlerOptions): Middleware;
|
|
|
562
664
|
* ```
|
|
563
665
|
*/
|
|
564
666
|
declare function notFoundHandler(message?: string): Middleware;
|
|
565
|
-
/**
|
|
566
|
-
* Catch async errors wrapper for route handlers
|
|
567
|
-
*
|
|
568
|
-
* @example
|
|
569
|
-
* ```typescript
|
|
570
|
-
* router.get('/users/:id', catchAsync(async (ctx) => {
|
|
571
|
-
* const user = await db.users.findById(ctx.params.id);
|
|
572
|
-
* if (!user) throw new NotFoundError('User not found');
|
|
573
|
-
* ctx.json(user);
|
|
574
|
-
* }));
|
|
575
|
-
* ```
|
|
576
|
-
*
|
|
577
|
-
* @deprecated This wrapper is redundant — async errors propagate naturally
|
|
578
|
-
* through the middleware chain and are caught by `errorHandler()`. Use the
|
|
579
|
-
* handler directly instead.
|
|
580
|
-
*/
|
|
581
|
-
declare function catchAsync(handler: (ctx: Context, next: Next) => Promise<void>): Middleware;
|
|
582
667
|
|
|
583
|
-
export { BadGatewayError, BadRequestError, ConflictError,
|
|
668
|
+
export { BadGatewayError, BadRequestError, ConflictError, ERROR_CODES, type ErrorHandlerOptions, ExpectationFailedError, FailedDependencyError, ForbiddenError, GENERIC_ERROR_CODE, GatewayTimeoutError, GoneError, HeaderValidationError, HttpError, type HttpErrorOptions, HttpVersionNotSupportedError, ImATeapotError, InsufficientStorageError, InternalServerError, InvalidEmailError, InvalidUrlError, LengthError, LengthRequiredError, LockedError, LoopDetectedError, MethodNotAllowedError, NetworkAuthRequiredError, NextRushError, NotAcceptableError, NotExtendedError, NotFoundError, NotImplementedError, PatternError, PayloadTooLargeError, PaymentRequiredError, PreconditionFailedError, PreconditionRequiredError, ProxyAuthRequiredError, RangeNotSatisfiableError, RangeValidationError, RequestHeaderFieldsTooLargeError, RequestTimeoutError, RequiredFieldError, ServiceUnavailableError, TooEarlyError, TooManyRequestsError, TypeMismatchError, UnauthorizedError, UnavailableForLegalReasonsError, UnprocessableEntityError, UnsupportedMediaTypeError, UpgradeRequiredError, UriTooLongError, VALIDATION_ERROR_CODE, ValidationError, type ValidationIssue, VariantAlsoNegotiatesError, badGateway, badRequest, codeForStatus, conflict, createError, errorHandler, forbidden, gatewayTimeout, getErrorStatus, getHttpStatusMessage, getSafeErrorMessage, internalError, isHttpError, methodNotAllowed, notFound, notFoundHandler, serviceUnavailable, tooManyRequests, unauthorized, unprocessableEntity };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,76 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
|
+
// src/codes.ts
|
|
5
|
+
var ERROR_CODES = Object.freeze({
|
|
6
|
+
400: "BAD_REQUEST",
|
|
7
|
+
401: "UNAUTHORIZED",
|
|
8
|
+
402: "PAYMENT_REQUIRED",
|
|
9
|
+
403: "FORBIDDEN",
|
|
10
|
+
404: "NOT_FOUND",
|
|
11
|
+
405: "METHOD_NOT_ALLOWED",
|
|
12
|
+
406: "NOT_ACCEPTABLE",
|
|
13
|
+
407: "PROXY_AUTH_REQUIRED",
|
|
14
|
+
408: "REQUEST_TIMEOUT",
|
|
15
|
+
409: "CONFLICT",
|
|
16
|
+
410: "GONE",
|
|
17
|
+
411: "LENGTH_REQUIRED",
|
|
18
|
+
412: "PRECONDITION_FAILED",
|
|
19
|
+
413: "PAYLOAD_TOO_LARGE",
|
|
20
|
+
414: "URI_TOO_LONG",
|
|
21
|
+
415: "UNSUPPORTED_MEDIA_TYPE",
|
|
22
|
+
416: "RANGE_NOT_SATISFIABLE",
|
|
23
|
+
417: "EXPECTATION_FAILED",
|
|
24
|
+
418: "IM_A_TEAPOT",
|
|
25
|
+
422: "UNPROCESSABLE_ENTITY",
|
|
26
|
+
423: "LOCKED",
|
|
27
|
+
424: "FAILED_DEPENDENCY",
|
|
28
|
+
425: "TOO_EARLY",
|
|
29
|
+
426: "UPGRADE_REQUIRED",
|
|
30
|
+
428: "PRECONDITION_REQUIRED",
|
|
31
|
+
429: "TOO_MANY_REQUESTS",
|
|
32
|
+
431: "REQUEST_HEADER_FIELDS_TOO_LARGE",
|
|
33
|
+
451: "UNAVAILABLE_FOR_LEGAL_REASONS",
|
|
34
|
+
500: "INTERNAL_SERVER_ERROR",
|
|
35
|
+
501: "NOT_IMPLEMENTED",
|
|
36
|
+
502: "BAD_GATEWAY",
|
|
37
|
+
503: "SERVICE_UNAVAILABLE",
|
|
38
|
+
504: "GATEWAY_TIMEOUT",
|
|
39
|
+
505: "HTTP_VERSION_NOT_SUPPORTED",
|
|
40
|
+
506: "VARIANT_ALSO_NEGOTIATES",
|
|
41
|
+
507: "INSUFFICIENT_STORAGE",
|
|
42
|
+
508: "LOOP_DETECTED",
|
|
43
|
+
510: "NOT_EXTENDED",
|
|
44
|
+
511: "NETWORK_AUTH_REQUIRED"
|
|
45
|
+
});
|
|
46
|
+
function codeForStatus(status) {
|
|
47
|
+
return ERROR_CODES[status] ?? `HTTP_${status}`;
|
|
48
|
+
}
|
|
49
|
+
__name(codeForStatus, "codeForStatus");
|
|
50
|
+
var GENERIC_ERROR_CODE = "INTERNAL_ERROR";
|
|
51
|
+
var VALIDATION_ERROR_CODE = "VALIDATION_ERROR";
|
|
52
|
+
|
|
4
53
|
// src/base.ts
|
|
5
54
|
var V8Error = Error;
|
|
6
|
-
var
|
|
55
|
+
var MAX_CAUSE_DEPTH = 5;
|
|
56
|
+
function serializeCause(cause, seen, depth) {
|
|
57
|
+
if (cause === void 0 || cause === null || depth >= MAX_CAUSE_DEPTH) return void 0;
|
|
58
|
+
if (typeof cause !== "object") return {
|
|
59
|
+
message: String(cause)
|
|
60
|
+
};
|
|
61
|
+
if (seen.has(cause)) return void 0;
|
|
62
|
+
seen.add(cause);
|
|
63
|
+
const err = cause;
|
|
64
|
+
const out = {};
|
|
65
|
+
if (typeof err.name === "string") out.name = err.name;
|
|
66
|
+
if (typeof err.message === "string") out.message = err.message;
|
|
67
|
+
if (typeof err.code === "string") out.code = err.code;
|
|
68
|
+
const nested = serializeCause(err.cause, seen, depth + 1);
|
|
69
|
+
if (nested !== void 0) out.cause = nested;
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
__name(serializeCause, "serializeCause");
|
|
73
|
+
var NextRushError = class _NextRushError extends Error {
|
|
7
74
|
static {
|
|
8
75
|
__name(this, "NextRushError");
|
|
9
76
|
}
|
|
@@ -17,14 +84,27 @@ var NextRushError = class extends Error {
|
|
|
17
84
|
details;
|
|
18
85
|
/** Original error that caused this error */
|
|
19
86
|
cause;
|
|
87
|
+
/** Correlation/request identifier for tracing across boundaries (audit E-5). */
|
|
88
|
+
requestId;
|
|
89
|
+
/** Distributed-trace identifier (audit E-5). */
|
|
90
|
+
traceId;
|
|
91
|
+
/** ISO-8601 timestamp of when the error was created, when supplied. */
|
|
92
|
+
timestamp;
|
|
20
93
|
constructor(message, options = {}) {
|
|
21
|
-
super(message
|
|
94
|
+
super(message, options.cause !== void 0 ? {
|
|
95
|
+
cause: options.cause
|
|
96
|
+
} : void 0);
|
|
22
97
|
this.name = this.constructor.name;
|
|
23
98
|
this.status = options.status ?? 500;
|
|
24
99
|
this.code = options.code ?? "INTERNAL_ERROR";
|
|
25
100
|
this.expose = options.expose ?? this.status < 500;
|
|
26
|
-
this.details = options.details
|
|
101
|
+
this.details = options.details ? Object.freeze({
|
|
102
|
+
...options.details
|
|
103
|
+
}) : void 0;
|
|
27
104
|
this.cause = options.cause;
|
|
105
|
+
this.requestId = options.requestId;
|
|
106
|
+
this.traceId = options.traceId;
|
|
107
|
+
this.timestamp = options.timestamp;
|
|
28
108
|
if (V8Error.captureStackTrace && !(this.expose && this.status < 500)) {
|
|
29
109
|
V8Error.captureStackTrace(this, this.constructor);
|
|
30
110
|
}
|
|
@@ -42,9 +122,50 @@ var NextRushError = class extends Error {
|
|
|
42
122
|
if (this.expose && this.details) {
|
|
43
123
|
json.details = this.details;
|
|
44
124
|
}
|
|
125
|
+
if (this.expose && this.cause !== void 0) {
|
|
126
|
+
const serialized = serializeCause(this.cause, /* @__PURE__ */ new Set(), 0);
|
|
127
|
+
if (serialized !== void 0) {
|
|
128
|
+
json.cause = serialized;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (this.requestId !== void 0) json.requestId = this.requestId;
|
|
132
|
+
if (this.traceId !== void 0) json.traceId = this.traceId;
|
|
133
|
+
if (this.timestamp !== void 0) json.timestamp = this.timestamp;
|
|
45
134
|
return json;
|
|
46
135
|
}
|
|
47
136
|
/**
|
|
137
|
+
* Reconstruct a {@link NextRushError} from a serialized {@link toJSON} payload
|
|
138
|
+
* (audit E-7).
|
|
139
|
+
*
|
|
140
|
+
* @remarks
|
|
141
|
+
* Enables cross-service transport: a downstream service can rebuild a typed
|
|
142
|
+
* error (with a working `instanceof NextRushError`) from the JSON it received,
|
|
143
|
+
* rather than being left with an opaque plain object. `message` falls back to
|
|
144
|
+
* a generic string when the source error was not exposed.
|
|
145
|
+
*
|
|
146
|
+
* @param json - A payload previously produced by {@link toJSON}.
|
|
147
|
+
* @returns A reconstructed {@link NextRushError}.
|
|
148
|
+
*/
|
|
149
|
+
static fromJSON(json) {
|
|
150
|
+
return _NextRushError.hydrate(json, (message, options) => new _NextRushError(message, options));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Shared hydration logic for {@link fromJSON} across the hierarchy.
|
|
154
|
+
* @internal
|
|
155
|
+
*/
|
|
156
|
+
static hydrate(json, build) {
|
|
157
|
+
const status = typeof json.status === "number" ? json.status : 500;
|
|
158
|
+
const message = typeof json.message === "string" ? json.message : getHttpStatusMessage(status);
|
|
159
|
+
return build(message, {
|
|
160
|
+
status,
|
|
161
|
+
code: typeof json.code === "string" ? json.code : void 0,
|
|
162
|
+
details: json.details !== null && typeof json.details === "object" ? json.details : void 0,
|
|
163
|
+
requestId: typeof json.requestId === "string" ? json.requestId : void 0,
|
|
164
|
+
traceId: typeof json.traceId === "string" ? json.traceId : void 0,
|
|
165
|
+
timestamp: typeof json.timestamp === "string" ? json.timestamp : void 0
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
48
169
|
* Create a response-safe version of the error
|
|
49
170
|
*/
|
|
50
171
|
toResponse() {
|
|
@@ -99,17 +220,50 @@ function getHttpStatusMessage(status) {
|
|
|
99
220
|
return HTTP_STATUS_MESSAGES[status] ?? `HTTP Error ${status}`;
|
|
100
221
|
}
|
|
101
222
|
__name(getHttpStatusMessage, "getHttpStatusMessage");
|
|
102
|
-
var HttpError = class extends NextRushError {
|
|
223
|
+
var HttpError = class _HttpError extends NextRushError {
|
|
103
224
|
static {
|
|
104
225
|
__name(this, "HttpError");
|
|
105
226
|
}
|
|
106
227
|
constructor(status, message, options = {}) {
|
|
107
228
|
super(message ?? getHttpStatusMessage(status), {
|
|
108
229
|
status,
|
|
109
|
-
code: options.code ??
|
|
230
|
+
code: options.code ?? codeForStatus(status),
|
|
110
231
|
expose: options.expose ?? status < 500,
|
|
111
232
|
details: options.details,
|
|
112
|
-
cause: options.cause
|
|
233
|
+
cause: options.cause,
|
|
234
|
+
requestId: options.requestId,
|
|
235
|
+
traceId: options.traceId,
|
|
236
|
+
timestamp: options.timestamp
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Reconstruct an {@link HttpError} from a serialized {@link NextRushError.toJSON}
|
|
241
|
+
* payload (audit E-7).
|
|
242
|
+
*
|
|
243
|
+
* @param json - A payload previously produced by `toJSON()`.
|
|
244
|
+
* @returns A reconstructed {@link HttpError}.
|
|
245
|
+
*/
|
|
246
|
+
static fromJSON(json) {
|
|
247
|
+
return NextRushError.hydrate(json, (message, options) => new _HttpError(options.status ?? 500, message, {
|
|
248
|
+
code: options.code,
|
|
249
|
+
details: options.details,
|
|
250
|
+
requestId: options.requestId,
|
|
251
|
+
traceId: options.traceId,
|
|
252
|
+
timestamp: options.timestamp
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/header-validation.ts
|
|
258
|
+
var HeaderValidationError = class extends NextRushError {
|
|
259
|
+
static {
|
|
260
|
+
__name(this, "HeaderValidationError");
|
|
261
|
+
}
|
|
262
|
+
constructor(message) {
|
|
263
|
+
super(message, {
|
|
264
|
+
status: 500,
|
|
265
|
+
code: "HEADER_VALIDATION_ERROR",
|
|
266
|
+
expose: false
|
|
113
267
|
});
|
|
114
268
|
}
|
|
115
269
|
};
|
|
@@ -581,7 +735,9 @@ var ValidationError = class _ValidationError extends NextRushError {
|
|
|
581
735
|
code: "VALIDATION_ERROR",
|
|
582
736
|
expose: true
|
|
583
737
|
});
|
|
584
|
-
this.issues =
|
|
738
|
+
this.issues = Object.freeze([
|
|
739
|
+
...issues
|
|
740
|
+
]);
|
|
585
741
|
}
|
|
586
742
|
/**
|
|
587
743
|
* Create from a single field error
|
|
@@ -777,16 +933,42 @@ var InvalidUrlError = class extends ValidationError {
|
|
|
777
933
|
var ERROR_MAP = {
|
|
778
934
|
400: BadRequestError,
|
|
779
935
|
401: UnauthorizedError,
|
|
936
|
+
402: PaymentRequiredError,
|
|
780
937
|
403: ForbiddenError,
|
|
781
938
|
404: NotFoundError,
|
|
939
|
+
406: NotAcceptableError,
|
|
940
|
+
407: ProxyAuthRequiredError,
|
|
941
|
+
408: RequestTimeoutError,
|
|
782
942
|
409: ConflictError,
|
|
943
|
+
410: GoneError,
|
|
944
|
+
411: LengthRequiredError,
|
|
945
|
+
412: PreconditionFailedError,
|
|
946
|
+
413: PayloadTooLargeError,
|
|
947
|
+
414: UriTooLongError,
|
|
948
|
+
415: UnsupportedMediaTypeError,
|
|
949
|
+
416: RangeNotSatisfiableError,
|
|
950
|
+
417: ExpectationFailedError,
|
|
951
|
+
418: ImATeapotError,
|
|
783
952
|
422: UnprocessableEntityError,
|
|
953
|
+
423: LockedError,
|
|
954
|
+
424: FailedDependencyError,
|
|
955
|
+
425: TooEarlyError,
|
|
956
|
+
426: UpgradeRequiredError,
|
|
957
|
+
428: PreconditionRequiredError,
|
|
784
958
|
429: TooManyRequestsError,
|
|
959
|
+
431: RequestHeaderFieldsTooLargeError,
|
|
960
|
+
451: UnavailableForLegalReasonsError,
|
|
785
961
|
500: InternalServerError,
|
|
786
962
|
501: NotImplementedError,
|
|
787
963
|
502: BadGatewayError,
|
|
788
964
|
503: ServiceUnavailableError,
|
|
789
|
-
504: GatewayTimeoutError
|
|
965
|
+
504: GatewayTimeoutError,
|
|
966
|
+
505: HttpVersionNotSupportedError,
|
|
967
|
+
506: VariantAlsoNegotiatesError,
|
|
968
|
+
507: InsufficientStorageError,
|
|
969
|
+
508: LoopDetectedError,
|
|
970
|
+
510: NotExtendedError,
|
|
971
|
+
511: NetworkAuthRequiredError
|
|
790
972
|
};
|
|
791
973
|
function createError(status, message, options) {
|
|
792
974
|
if (status === 405) {
|
|
@@ -871,6 +1053,7 @@ function getSafeErrorMessage(error) {
|
|
|
871
1053
|
__name(getSafeErrorMessage, "getSafeErrorMessage");
|
|
872
1054
|
|
|
873
1055
|
// src/middleware.ts
|
|
1056
|
+
import { SECURITY_AUDIT } from "@nextrush/types";
|
|
874
1057
|
function defaultLogger(error, ctx) {
|
|
875
1058
|
const status = error instanceof HttpError ? error.status : 500;
|
|
876
1059
|
if (status >= 500) {
|
|
@@ -881,17 +1064,18 @@ function defaultLogger(error, ctx) {
|
|
|
881
1064
|
}
|
|
882
1065
|
__name(defaultLogger, "defaultLogger");
|
|
883
1066
|
function errorHandler(options = {}) {
|
|
884
|
-
const { includeStack = false, logger = defaultLogger, transform, handlers } = options;
|
|
885
|
-
|
|
1067
|
+
const { includeStack = false, isProduction = false, logger = defaultLogger, transform, handlers } = options;
|
|
1068
|
+
let warnedIncludeStackInProduction = false;
|
|
1069
|
+
const handler = /* @__PURE__ */ __name(async (ctx, next) => {
|
|
886
1070
|
try {
|
|
887
1071
|
await next();
|
|
888
1072
|
} catch (error) {
|
|
889
1073
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
890
1074
|
logger(err, ctx);
|
|
891
1075
|
if (handlers) {
|
|
892
|
-
for (const [ErrorType,
|
|
1076
|
+
for (const [ErrorType, handler2] of handlers) {
|
|
893
1077
|
if (err instanceof ErrorType) {
|
|
894
|
-
|
|
1078
|
+
handler2(err, ctx);
|
|
895
1079
|
return;
|
|
896
1080
|
}
|
|
897
1081
|
}
|
|
@@ -910,6 +1094,8 @@ function errorHandler(options = {}) {
|
|
|
910
1094
|
let body;
|
|
911
1095
|
if (transform) {
|
|
912
1096
|
body = transform(err, ctx);
|
|
1097
|
+
} else if (err instanceof NextRushError) {
|
|
1098
|
+
body = err.toJSON();
|
|
913
1099
|
} else {
|
|
914
1100
|
body = {
|
|
915
1101
|
error: expose ? err.name : getHttpStatusMessage(status),
|
|
@@ -920,13 +1106,37 @@ function errorHandler(options = {}) {
|
|
|
920
1106
|
if (expose && details) {
|
|
921
1107
|
body.details = details;
|
|
922
1108
|
}
|
|
923
|
-
|
|
1109
|
+
}
|
|
1110
|
+
if (includeStack && err.stack) {
|
|
1111
|
+
if (isProduction) {
|
|
1112
|
+
if (!warnedIncludeStackInProduction) {
|
|
1113
|
+
warnedIncludeStackInProduction = true;
|
|
1114
|
+
console.warn("[@nextrush/errors] includeStack: true was ignored because isProduction is true. Stack traces are never emitted in production regardless of configuration.");
|
|
1115
|
+
}
|
|
1116
|
+
} else {
|
|
924
1117
|
body.stack = err.stack.split("\n").map((line) => line.trim());
|
|
925
1118
|
}
|
|
926
1119
|
}
|
|
927
1120
|
ctx.json(body);
|
|
928
1121
|
}
|
|
929
|
-
};
|
|
1122
|
+
}, "handler");
|
|
1123
|
+
function auditVerdict() {
|
|
1124
|
+
if (includeStack && !isProduction) {
|
|
1125
|
+
return {
|
|
1126
|
+
level: "warn",
|
|
1127
|
+
message: "errorHandler({ includeStack: true }) was constructed without isProduction \u2014 the per-request guard that ignores includeStack in production never fires unless isProduction is explicitly threaded through (e.g. isProduction: app.isProduction). Wire it, or this instance will leak stack traces in production."
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
level: "ok"
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
__name(auditVerdict, "auditVerdict");
|
|
1135
|
+
Object.defineProperty(handler, SECURITY_AUDIT, {
|
|
1136
|
+
value: auditVerdict,
|
|
1137
|
+
enumerable: false
|
|
1138
|
+
});
|
|
1139
|
+
return handler;
|
|
930
1140
|
}
|
|
931
1141
|
__name(errorHandler, "errorHandler");
|
|
932
1142
|
function notFoundHandler(message = "Not Found") {
|
|
@@ -943,19 +1153,18 @@ function notFoundHandler(message = "Not Found") {
|
|
|
943
1153
|
};
|
|
944
1154
|
}
|
|
945
1155
|
__name(notFoundHandler, "notFoundHandler");
|
|
946
|
-
function catchAsync(handler) {
|
|
947
|
-
return handler;
|
|
948
|
-
}
|
|
949
|
-
__name(catchAsync, "catchAsync");
|
|
950
1156
|
export {
|
|
951
1157
|
BadGatewayError,
|
|
952
1158
|
BadRequestError,
|
|
953
1159
|
ConflictError,
|
|
1160
|
+
ERROR_CODES,
|
|
954
1161
|
ExpectationFailedError,
|
|
955
1162
|
FailedDependencyError,
|
|
956
1163
|
ForbiddenError,
|
|
1164
|
+
GENERIC_ERROR_CODE,
|
|
957
1165
|
GatewayTimeoutError,
|
|
958
1166
|
GoneError,
|
|
1167
|
+
HeaderValidationError,
|
|
959
1168
|
HttpError,
|
|
960
1169
|
HttpVersionNotSupportedError,
|
|
961
1170
|
ImATeapotError,
|
|
@@ -995,11 +1204,12 @@ export {
|
|
|
995
1204
|
UnsupportedMediaTypeError,
|
|
996
1205
|
UpgradeRequiredError,
|
|
997
1206
|
UriTooLongError,
|
|
1207
|
+
VALIDATION_ERROR_CODE,
|
|
998
1208
|
ValidationError,
|
|
999
1209
|
VariantAlsoNegotiatesError,
|
|
1000
1210
|
badGateway,
|
|
1001
1211
|
badRequest,
|
|
1002
|
-
|
|
1212
|
+
codeForStatus,
|
|
1003
1213
|
conflict,
|
|
1004
1214
|
createError,
|
|
1005
1215
|
errorHandler,
|