@nextrush/errors 3.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/LICENSE +21 -0
- package/README.md +681 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +1021 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/__tests__/base.test.ts +180 -0
- package/src/__tests__/factory.test.ts +312 -0
- package/src/__tests__/http-errors.test.ts +458 -0
- package/src/__tests__/middleware.test.ts +385 -0
- package/src/__tests__/validation.test.ts +315 -0
- package/src/base.ts +162 -0
- package/src/factory.ts +205 -0
- package/src/http-errors.ts +409 -0
- package/src/index.ts +101 -0
- package/src/middleware.ts +183 -0
- package/src/validation.ts +210 -0
package/src/base.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/errors - Base Error Classes
|
|
3
|
+
*
|
|
4
|
+
* Foundational error classes for NextRush framework.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const V8Error = Error as ErrorConstructor & {
|
|
10
|
+
captureStackTrace?: (targetObject: object, constructorOpt?: Function) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base error class for all NextRush errors
|
|
15
|
+
*/
|
|
16
|
+
export class NextRushError extends Error {
|
|
17
|
+
/** HTTP status code */
|
|
18
|
+
readonly status: number;
|
|
19
|
+
|
|
20
|
+
/** Error code for programmatic handling */
|
|
21
|
+
readonly code: string;
|
|
22
|
+
|
|
23
|
+
/** Whether error message is safe to expose to client */
|
|
24
|
+
readonly expose: boolean;
|
|
25
|
+
|
|
26
|
+
/** Additional error details */
|
|
27
|
+
readonly details?: Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
/** Original error that caused this error */
|
|
30
|
+
readonly cause?: unknown;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
message: string,
|
|
34
|
+
options: {
|
|
35
|
+
status?: number;
|
|
36
|
+
code?: string;
|
|
37
|
+
expose?: boolean;
|
|
38
|
+
details?: Record<string, unknown>;
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
} = {}
|
|
41
|
+
) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = this.constructor.name;
|
|
44
|
+
this.status = options.status ?? 500;
|
|
45
|
+
this.code = options.code ?? 'INTERNAL_ERROR';
|
|
46
|
+
this.expose = options.expose ?? this.status < 500;
|
|
47
|
+
this.details = options.details;
|
|
48
|
+
this.cause = options.cause;
|
|
49
|
+
|
|
50
|
+
// Maintain proper stack trace for V8
|
|
51
|
+
// Skip for common client errors (4xx with expose=true) to reduce overhead.
|
|
52
|
+
// These are expected control-flow errors, not bugs — stack traces add cost
|
|
53
|
+
// without diagnostic value in production.
|
|
54
|
+
if (V8Error.captureStackTrace && !(this.expose && this.status < 500)) {
|
|
55
|
+
V8Error.captureStackTrace(this, this.constructor);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert error to JSON representation
|
|
61
|
+
*/
|
|
62
|
+
toJSON(): Record<string, unknown> {
|
|
63
|
+
const json: Record<string, unknown> = {
|
|
64
|
+
error: this.name,
|
|
65
|
+
message: this.expose ? this.message : 'Internal Server Error',
|
|
66
|
+
code: this.code,
|
|
67
|
+
status: this.status,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (this.expose && this.details) {
|
|
71
|
+
json.details = this.details;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return json;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a response-safe version of the error
|
|
79
|
+
*/
|
|
80
|
+
toResponse(): { status: number; body: Record<string, unknown> } {
|
|
81
|
+
return {
|
|
82
|
+
status: this.status,
|
|
83
|
+
body: this.toJSON(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Default HTTP status messages
|
|
90
|
+
*/
|
|
91
|
+
const HTTP_STATUS_MESSAGES: Record<number, string> = {
|
|
92
|
+
400: 'Bad Request',
|
|
93
|
+
401: 'Unauthorized',
|
|
94
|
+
402: 'Payment Required',
|
|
95
|
+
403: 'Forbidden',
|
|
96
|
+
404: 'Not Found',
|
|
97
|
+
405: 'Method Not Allowed',
|
|
98
|
+
406: 'Not Acceptable',
|
|
99
|
+
407: 'Proxy Authentication Required',
|
|
100
|
+
408: 'Request Timeout',
|
|
101
|
+
409: 'Conflict',
|
|
102
|
+
410: 'Gone',
|
|
103
|
+
411: 'Length Required',
|
|
104
|
+
412: 'Precondition Failed',
|
|
105
|
+
413: 'Payload Too Large',
|
|
106
|
+
414: 'URI Too Long',
|
|
107
|
+
415: 'Unsupported Media Type',
|
|
108
|
+
416: 'Range Not Satisfiable',
|
|
109
|
+
417: 'Expectation Failed',
|
|
110
|
+
418: "I'm a Teapot",
|
|
111
|
+
422: 'Unprocessable Entity',
|
|
112
|
+
423: 'Locked',
|
|
113
|
+
424: 'Failed Dependency',
|
|
114
|
+
425: 'Too Early',
|
|
115
|
+
426: 'Upgrade Required',
|
|
116
|
+
428: 'Precondition Required',
|
|
117
|
+
429: 'Too Many Requests',
|
|
118
|
+
431: 'Request Header Fields Too Large',
|
|
119
|
+
451: 'Unavailable For Legal Reasons',
|
|
120
|
+
500: 'Internal Server Error',
|
|
121
|
+
501: 'Not Implemented',
|
|
122
|
+
502: 'Bad Gateway',
|
|
123
|
+
503: 'Service Unavailable',
|
|
124
|
+
504: 'Gateway Timeout',
|
|
125
|
+
505: 'HTTP Version Not Supported',
|
|
126
|
+
506: 'Variant Also Negotiates',
|
|
127
|
+
507: 'Insufficient Storage',
|
|
128
|
+
508: 'Loop Detected',
|
|
129
|
+
510: 'Not Extended',
|
|
130
|
+
511: 'Network Authentication Required',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get default message for an HTTP status code
|
|
135
|
+
*/
|
|
136
|
+
export function getHttpStatusMessage(status: number): string {
|
|
137
|
+
return HTTP_STATUS_MESSAGES[status] ?? `HTTP Error ${status}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Base class for HTTP errors
|
|
142
|
+
*/
|
|
143
|
+
export class HttpError extends NextRushError {
|
|
144
|
+
constructor(
|
|
145
|
+
status: number,
|
|
146
|
+
message?: string,
|
|
147
|
+
options: {
|
|
148
|
+
code?: string;
|
|
149
|
+
expose?: boolean;
|
|
150
|
+
details?: Record<string, unknown>;
|
|
151
|
+
cause?: unknown;
|
|
152
|
+
} = {}
|
|
153
|
+
) {
|
|
154
|
+
super(message ?? getHttpStatusMessage(status), {
|
|
155
|
+
status,
|
|
156
|
+
code: options.code ?? `HTTP_${status}`,
|
|
157
|
+
expose: options.expose ?? status < 500,
|
|
158
|
+
details: options.details,
|
|
159
|
+
cause: options.cause,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nextrush/errors - Error Factory
|
|
3
|
+
*
|
|
4
|
+
* Factory functions for creating errors.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { HttpError, NextRushError } from './base';
|
|
10
|
+
import {
|
|
11
|
+
BadGatewayError,
|
|
12
|
+
BadRequestError,
|
|
13
|
+
ConflictError,
|
|
14
|
+
ForbiddenError,
|
|
15
|
+
GatewayTimeoutError,
|
|
16
|
+
InternalServerError,
|
|
17
|
+
MethodNotAllowedError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
NotImplementedError,
|
|
20
|
+
ServiceUnavailableError,
|
|
21
|
+
TooManyRequestsError,
|
|
22
|
+
UnauthorizedError,
|
|
23
|
+
UnprocessableEntityError,
|
|
24
|
+
type HttpErrorOptions,
|
|
25
|
+
} from './http-errors';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* HTTP status code to error class mapping
|
|
29
|
+
*/
|
|
30
|
+
const ERROR_MAP: Record<number, new (message?: string, options?: HttpErrorOptions) => HttpError> = {
|
|
31
|
+
400: BadRequestError,
|
|
32
|
+
401: UnauthorizedError,
|
|
33
|
+
403: ForbiddenError,
|
|
34
|
+
404: NotFoundError,
|
|
35
|
+
409: ConflictError,
|
|
36
|
+
422: UnprocessableEntityError,
|
|
37
|
+
429: TooManyRequestsError,
|
|
38
|
+
500: InternalServerError,
|
|
39
|
+
501: NotImplementedError,
|
|
40
|
+
502: BadGatewayError,
|
|
41
|
+
503: ServiceUnavailableError,
|
|
42
|
+
504: GatewayTimeoutError,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create an HTTP error by status code
|
|
47
|
+
*/
|
|
48
|
+
export function createError(
|
|
49
|
+
status: number,
|
|
50
|
+
message?: string,
|
|
51
|
+
options?: HttpErrorOptions
|
|
52
|
+
): HttpError {
|
|
53
|
+
// Special case: MethodNotAllowedError requires allowedMethods array
|
|
54
|
+
if (status === 405) {
|
|
55
|
+
return new MethodNotAllowedError([], message, options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ErrorClass = ERROR_MAP[status];
|
|
59
|
+
|
|
60
|
+
if (ErrorClass) {
|
|
61
|
+
return new ErrorClass(message, options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback: let HttpError constructor resolve the message via getHttpStatusMessage()
|
|
65
|
+
return new HttpError(status, message, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a 400 Bad Request error
|
|
70
|
+
*/
|
|
71
|
+
export function badRequest(message?: string, options?: HttpErrorOptions): BadRequestError {
|
|
72
|
+
return new BadRequestError(message, options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a 401 Unauthorized error
|
|
77
|
+
*/
|
|
78
|
+
export function unauthorized(message?: string, options?: HttpErrorOptions): UnauthorizedError {
|
|
79
|
+
return new UnauthorizedError(message, options);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a 403 Forbidden error
|
|
84
|
+
*/
|
|
85
|
+
export function forbidden(message?: string, options?: HttpErrorOptions): ForbiddenError {
|
|
86
|
+
return new ForbiddenError(message, options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a 404 Not Found error
|
|
91
|
+
*/
|
|
92
|
+
export function notFound(message?: string, options?: HttpErrorOptions): NotFoundError {
|
|
93
|
+
return new NotFoundError(message, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a 405 Method Not Allowed error
|
|
98
|
+
*/
|
|
99
|
+
export function methodNotAllowed(
|
|
100
|
+
allowedMethods?: string[],
|
|
101
|
+
message?: string,
|
|
102
|
+
options?: HttpErrorOptions
|
|
103
|
+
): MethodNotAllowedError {
|
|
104
|
+
return new MethodNotAllowedError(allowedMethods, message, options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a 409 Conflict error
|
|
109
|
+
*/
|
|
110
|
+
export function conflict(message?: string, options?: HttpErrorOptions): ConflictError {
|
|
111
|
+
return new ConflictError(message, options);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a 422 Unprocessable Entity error
|
|
116
|
+
*/
|
|
117
|
+
export function unprocessableEntity(
|
|
118
|
+
message?: string,
|
|
119
|
+
options?: HttpErrorOptions
|
|
120
|
+
): UnprocessableEntityError {
|
|
121
|
+
return new UnprocessableEntityError(message, options);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a 429 Too Many Requests error
|
|
126
|
+
*/
|
|
127
|
+
export function tooManyRequests(
|
|
128
|
+
message?: string,
|
|
129
|
+
options?: HttpErrorOptions & { retryAfter?: number }
|
|
130
|
+
): TooManyRequestsError {
|
|
131
|
+
return new TooManyRequestsError(message, options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a 500 Internal Server Error
|
|
136
|
+
*/
|
|
137
|
+
export function internalError(message?: string, options?: HttpErrorOptions): InternalServerError {
|
|
138
|
+
return new InternalServerError(message, options);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a 502 Bad Gateway error
|
|
143
|
+
*/
|
|
144
|
+
export function badGateway(message?: string, options?: HttpErrorOptions): BadGatewayError {
|
|
145
|
+
return new BadGatewayError(message, options);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a 503 Service Unavailable error
|
|
150
|
+
*/
|
|
151
|
+
export function serviceUnavailable(
|
|
152
|
+
message?: string,
|
|
153
|
+
options?: HttpErrorOptions & { retryAfter?: number }
|
|
154
|
+
): ServiceUnavailableError {
|
|
155
|
+
return new ServiceUnavailableError(message, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create a 504 Gateway Timeout error
|
|
160
|
+
*/
|
|
161
|
+
export function gatewayTimeout(message?: string, options?: HttpErrorOptions): GatewayTimeoutError {
|
|
162
|
+
return new GatewayTimeoutError(message, options);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if an error is a NextRush HttpError
|
|
167
|
+
*/
|
|
168
|
+
export function isHttpError(error: unknown): error is HttpError {
|
|
169
|
+
return error instanceof HttpError;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get HTTP status from any error
|
|
174
|
+
*
|
|
175
|
+
* Checks NextRushError first (covers HttpError, ValidationError, and all subclasses),
|
|
176
|
+
* then falls back to duck-typed status property.
|
|
177
|
+
*/
|
|
178
|
+
export function getErrorStatus(error: unknown): number {
|
|
179
|
+
if (error instanceof NextRushError) {
|
|
180
|
+
return error.status;
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
error != null &&
|
|
184
|
+
typeof error === 'object' &&
|
|
185
|
+
'status' in error &&
|
|
186
|
+
typeof (error as { status: unknown }).status === 'number'
|
|
187
|
+
) {
|
|
188
|
+
const status = (error as { status: number }).status;
|
|
189
|
+
return status >= 400 && status < 600 ? status : 500;
|
|
190
|
+
}
|
|
191
|
+
return 500;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get error message safe for client response
|
|
196
|
+
*
|
|
197
|
+
* Exposes message for any NextRushError (including ValidationError)
|
|
198
|
+
* when the error's expose flag is true.
|
|
199
|
+
*/
|
|
200
|
+
export function getSafeErrorMessage(error: unknown): string {
|
|
201
|
+
if (error instanceof NextRushError && error.expose) {
|
|
202
|
+
return error.message;
|
|
203
|
+
}
|
|
204
|
+
return 'Internal Server Error';
|
|
205
|
+
}
|