@batkit/express-middleware 0.1.3 → 0.1.5
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/dist/error-handler.d.ts.map +1 -1
- package/dist/error-handler.js +68 -54
- package/dist/error-handler.js.map +1 -1
- package/dist/index.cjs +71 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts.map +1 -1
- package/package.json +5 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAY/D,MAAM,WAAW,cAAc;IAI7B,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAKnC,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAAC;CAChD;AAKD,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IASlC,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB;IAmC9C,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,gBAAgB;IAcxB,OAAO,CAAC,cAAc;CAevB;AAKD,MAAM,WAAW,mBAAmB;IAIlC,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAK9B,SAAS,CAAC,EAAE,OAAO,CAAC;IAKpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClD;AA2ED,wBAAgB,YAAY,CAC1B,OAAO,GAAE,mBAAwB,GAEhC,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAiCzE;AAsBD,wBAAgB,YAAY,CAC1B,EAAE,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,GACxE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAI3D"}
|
package/dist/error-handler.js
CHANGED
|
@@ -2,6 +2,11 @@ import { isAppError } from "@batkit/errors";
|
|
|
2
2
|
import { LoggerFacade } from "@batkit/logger";
|
|
3
3
|
import { PROBLEM_DETAILS_CONTENT_TYPE, createExtendedProblemDetails, } from "@batkit/rfc9457";
|
|
4
4
|
const logger = LoggerFacade.getLogger("error-handler");
|
|
5
|
+
const HttpStatus = {
|
|
6
|
+
BAD_GATEWAY: 502,
|
|
7
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
8
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
9
|
+
};
|
|
5
10
|
export class DefaultErrorFormatter {
|
|
6
11
|
canFormat(error) {
|
|
7
12
|
return (isAppError(error) ||
|
|
@@ -21,17 +26,17 @@ export class DefaultErrorFormatter {
|
|
|
21
26
|
}
|
|
22
27
|
if (error instanceof Error) {
|
|
23
28
|
return createExtendedProblemDetails({
|
|
24
|
-
type: "error:internal",
|
|
25
|
-
title: "Internal Server Error",
|
|
26
|
-
status: 500,
|
|
27
29
|
detail: error.message,
|
|
30
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
31
|
+
title: "Internal Server Error",
|
|
32
|
+
type: "error:internal",
|
|
28
33
|
});
|
|
29
34
|
}
|
|
30
35
|
return createExtendedProblemDetails({
|
|
31
|
-
type: "error:unknown",
|
|
32
|
-
title: "Unknown Error",
|
|
33
|
-
status: 500,
|
|
34
36
|
detail: "An unknown error occurred",
|
|
37
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
38
|
+
title: "Unknown Error",
|
|
39
|
+
type: "error:unknown",
|
|
35
40
|
});
|
|
36
41
|
}
|
|
37
42
|
isAxiosError(error) {
|
|
@@ -47,78 +52,87 @@ export class DefaultErrorFormatter {
|
|
|
47
52
|
Array.isArray(error.issues));
|
|
48
53
|
}
|
|
49
54
|
formatAxiosError(error) {
|
|
50
|
-
const status = error.response?.status ||
|
|
55
|
+
const status = error.response?.status || HttpStatus.BAD_GATEWAY;
|
|
51
56
|
return createExtendedProblemDetails({
|
|
52
|
-
type: "error:upstream",
|
|
53
|
-
title: "Upstream Service Error",
|
|
54
|
-
status,
|
|
55
57
|
detail: error.message,
|
|
56
|
-
|
|
58
|
+
status,
|
|
59
|
+
title: "Upstream Service Error",
|
|
60
|
+
type: "error:upstream",
|
|
57
61
|
upstreamMethod: error.config?.method?.toUpperCase(),
|
|
58
62
|
upstreamStatus: error.response?.status,
|
|
63
|
+
upstreamUrl: error.config?.url,
|
|
59
64
|
});
|
|
60
65
|
}
|
|
61
66
|
formatZodError(error) {
|
|
62
67
|
const validationErrors = error.issues.map((issue) => ({
|
|
68
|
+
code: issue.code,
|
|
63
69
|
field: issue.path.join("."),
|
|
64
70
|
message: issue.message,
|
|
65
|
-
code: issue.code,
|
|
66
71
|
}));
|
|
67
72
|
return createExtendedProblemDetails({
|
|
68
|
-
type: "error:validation",
|
|
69
|
-
title: "Validation Error",
|
|
70
|
-
status: 422,
|
|
71
73
|
detail: "Request validation failed",
|
|
74
|
+
status: HttpStatus.UNPROCESSABLE_ENTITY,
|
|
75
|
+
title: "Validation Error",
|
|
76
|
+
type: "error:validation",
|
|
72
77
|
validationErrors,
|
|
73
78
|
});
|
|
74
79
|
}
|
|
75
80
|
}
|
|
81
|
+
function logHandledError(error, req, options) {
|
|
82
|
+
const { onError } = options;
|
|
83
|
+
if (onError) {
|
|
84
|
+
onError(error, req);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (isAppError(error) && error.isOperational) {
|
|
88
|
+
logger.warn("Operational error occurred", {
|
|
89
|
+
code: error.code,
|
|
90
|
+
error: error.message,
|
|
91
|
+
method: req.method,
|
|
92
|
+
path: req.path,
|
|
93
|
+
statusCode: error.statusCode,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const contentType = req.headers?.["content-type"];
|
|
98
|
+
const isJsonContentType = typeof contentType === "string" && /^application\/(?:[^;]+\+)?json/iu.test(contentType);
|
|
99
|
+
const isBinaryBody = Buffer.isBuffer(req.body) || (contentType !== undefined && !isJsonContentType);
|
|
100
|
+
let bodyLength;
|
|
101
|
+
if (Buffer.isBuffer(req.body)) {
|
|
102
|
+
bodyLength = req.body.length;
|
|
103
|
+
}
|
|
104
|
+
else if (typeof req.body === "string") {
|
|
105
|
+
bodyLength = Buffer.byteLength(req.body);
|
|
106
|
+
}
|
|
107
|
+
logger.error(error, {
|
|
108
|
+
method: req.method,
|
|
109
|
+
path: req.path,
|
|
110
|
+
query: req.query,
|
|
111
|
+
...(isBinaryBody ? { bodyContentType: contentType, bodyLength } : { body: req.body }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function buildProblemDetails(error, formatters) {
|
|
115
|
+
const formatter = formatters.find((candidate) => candidate.canFormat(error));
|
|
116
|
+
return formatter
|
|
117
|
+
? formatter.format(error)
|
|
118
|
+
: createExtendedProblemDetails({
|
|
119
|
+
detail: "An unexpected error occurred",
|
|
120
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
121
|
+
title: "Internal Server Error",
|
|
122
|
+
type: "error:internal",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
76
125
|
export function errorHandler(options = {}) {
|
|
77
|
-
const { formatters = [new DefaultErrorFormatter()], logErrors = true
|
|
126
|
+
const { formatters = [new DefaultErrorFormatter()], logErrors = true } = options;
|
|
78
127
|
return (error, req, res, next) => {
|
|
79
128
|
if (res.headersSent) {
|
|
80
129
|
next(error);
|
|
81
130
|
return;
|
|
82
131
|
}
|
|
83
132
|
if (logErrors) {
|
|
84
|
-
|
|
85
|
-
onError(error, req);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
if (isAppError(error) && error.isOperational) {
|
|
89
|
-
logger.warn("Operational error occurred", {
|
|
90
|
-
error: error.message,
|
|
91
|
-
code: error.code,
|
|
92
|
-
statusCode: error.statusCode,
|
|
93
|
-
path: req.path,
|
|
94
|
-
method: req.method,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
logger.error(error, {
|
|
99
|
-
path: req.path,
|
|
100
|
-
method: req.method,
|
|
101
|
-
query: req.query,
|
|
102
|
-
...(Buffer.isBuffer(req.body)
|
|
103
|
-
? { bodyContentType: req.headers["content-type"], bodyLength: req.body.length }
|
|
104
|
-
: { body: req.body }),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
let problemDetails;
|
|
110
|
-
const formatter = formatters.find((f) => f.canFormat(error));
|
|
111
|
-
if (formatter) {
|
|
112
|
-
problemDetails = formatter.format(error);
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
problemDetails = createExtendedProblemDetails({
|
|
116
|
-
type: "error:internal",
|
|
117
|
-
title: "Internal Server Error",
|
|
118
|
-
status: 500,
|
|
119
|
-
detail: "An unexpected error occurred",
|
|
120
|
-
});
|
|
133
|
+
logHandledError(error, req, options);
|
|
121
134
|
}
|
|
135
|
+
const problemDetails = buildProblemDetails(error, formatters);
|
|
122
136
|
if (error.stack) {
|
|
123
137
|
problemDetails.stack = error.stack.split("\n").map((line) => line.trim());
|
|
124
138
|
}
|
|
@@ -126,7 +140,7 @@ export function errorHandler(options = {}) {
|
|
|
126
140
|
problemDetails.instance = req.path;
|
|
127
141
|
}
|
|
128
142
|
res
|
|
129
|
-
.status(problemDetails.status ||
|
|
143
|
+
.status(problemDetails.status || HttpStatus.INTERNAL_SERVER_ERROR)
|
|
130
144
|
.set("Content-Type", PROBLEM_DETAILS_CONTENT_TYPE)
|
|
131
145
|
.json(problemDetails);
|
|
132
146
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAEL,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC;AAGzB,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,11 @@ let _batkit_rfc9457 = require("@batkit/rfc9457");
|
|
|
5
5
|
let _batkit_logger_async_local = require("@batkit/logger/async-local");
|
|
6
6
|
//#region src/error-handler.ts
|
|
7
7
|
const logger = _batkit_logger.LoggerFacade.getLogger("error-handler");
|
|
8
|
+
const HttpStatus = {
|
|
9
|
+
BAD_GATEWAY: 502,
|
|
10
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
11
|
+
UNPROCESSABLE_ENTITY: 422
|
|
12
|
+
};
|
|
8
13
|
/**
|
|
9
14
|
* Default error formatter that handles common error types
|
|
10
15
|
*/
|
|
@@ -17,16 +22,16 @@ var DefaultErrorFormatter = class {
|
|
|
17
22
|
if (this.isAxiosError(error)) return this.formatAxiosError(error);
|
|
18
23
|
if (this.isZodError(error)) return this.formatZodError(error);
|
|
19
24
|
if (error instanceof Error) return (0, _batkit_rfc9457.createExtendedProblemDetails)({
|
|
20
|
-
|
|
25
|
+
detail: error.message,
|
|
26
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
21
27
|
title: "Internal Server Error",
|
|
22
|
-
|
|
23
|
-
detail: error.message
|
|
28
|
+
type: "error:internal"
|
|
24
29
|
});
|
|
25
30
|
return (0, _batkit_rfc9457.createExtendedProblemDetails)({
|
|
26
|
-
|
|
31
|
+
detail: "An unknown error occurred",
|
|
32
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
27
33
|
title: "Unknown Error",
|
|
28
|
-
|
|
29
|
-
detail: "An unknown error occurred"
|
|
34
|
+
type: "error:unknown"
|
|
30
35
|
});
|
|
31
36
|
}
|
|
32
37
|
isAxiosError(error) {
|
|
@@ -36,30 +41,73 @@ var DefaultErrorFormatter = class {
|
|
|
36
41
|
return typeof error === "object" && error !== null && "issues" in error && Array.isArray(error.issues);
|
|
37
42
|
}
|
|
38
43
|
formatAxiosError(error) {
|
|
44
|
+
const status = error.response?.status || HttpStatus.BAD_GATEWAY;
|
|
39
45
|
return (0, _batkit_rfc9457.createExtendedProblemDetails)({
|
|
40
|
-
type: "error:upstream",
|
|
41
|
-
title: "Upstream Service Error",
|
|
42
|
-
status: error.response?.status || 502,
|
|
43
46
|
detail: error.message,
|
|
44
|
-
|
|
47
|
+
status,
|
|
48
|
+
title: "Upstream Service Error",
|
|
49
|
+
type: "error:upstream",
|
|
45
50
|
upstreamMethod: (error.config?.method)?.toUpperCase(),
|
|
46
|
-
upstreamStatus: error.response?.status
|
|
51
|
+
upstreamStatus: error.response?.status,
|
|
52
|
+
upstreamUrl: error.config?.url
|
|
47
53
|
});
|
|
48
54
|
}
|
|
49
55
|
formatZodError(error) {
|
|
56
|
+
const validationErrors = error.issues.map((issue) => ({
|
|
57
|
+
code: issue.code,
|
|
58
|
+
field: issue.path.join("."),
|
|
59
|
+
message: issue.message
|
|
60
|
+
}));
|
|
50
61
|
return (0, _batkit_rfc9457.createExtendedProblemDetails)({
|
|
51
|
-
type: "error:validation",
|
|
52
|
-
title: "Validation Error",
|
|
53
|
-
status: 422,
|
|
54
62
|
detail: "Request validation failed",
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}))
|
|
63
|
+
status: HttpStatus.UNPROCESSABLE_ENTITY,
|
|
64
|
+
title: "Validation Error",
|
|
65
|
+
type: "error:validation",
|
|
66
|
+
validationErrors
|
|
60
67
|
});
|
|
61
68
|
}
|
|
62
69
|
};
|
|
70
|
+
function logHandledError(error, req, options) {
|
|
71
|
+
const { onError } = options;
|
|
72
|
+
if (onError) {
|
|
73
|
+
onError(error, req);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if ((0, _batkit_errors.isAppError)(error) && error.isOperational) {
|
|
77
|
+
logger.warn("Operational error occurred", {
|
|
78
|
+
code: error.code,
|
|
79
|
+
error: error.message,
|
|
80
|
+
method: req.method,
|
|
81
|
+
path: req.path,
|
|
82
|
+
statusCode: error.statusCode
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const contentType = req.headers?.["content-type"];
|
|
87
|
+
const isJsonContentType = typeof contentType === "string" && /^application\/(?:[^;]+\+)?json/iu.test(contentType);
|
|
88
|
+
const isBinaryBody = Buffer.isBuffer(req.body) || contentType !== void 0 && !isJsonContentType;
|
|
89
|
+
let bodyLength;
|
|
90
|
+
if (Buffer.isBuffer(req.body)) bodyLength = req.body.length;
|
|
91
|
+
else if (typeof req.body === "string") bodyLength = Buffer.byteLength(req.body);
|
|
92
|
+
logger.error(error, {
|
|
93
|
+
method: req.method,
|
|
94
|
+
path: req.path,
|
|
95
|
+
query: req.query,
|
|
96
|
+
...isBinaryBody ? {
|
|
97
|
+
bodyContentType: contentType,
|
|
98
|
+
bodyLength
|
|
99
|
+
} : { body: req.body }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function buildProblemDetails(error, formatters) {
|
|
103
|
+
const formatter = formatters.find((candidate) => candidate.canFormat(error));
|
|
104
|
+
return formatter ? formatter.format(error) : (0, _batkit_rfc9457.createExtendedProblemDetails)({
|
|
105
|
+
detail: "An unexpected error occurred",
|
|
106
|
+
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
107
|
+
title: "Internal Server Error",
|
|
108
|
+
type: "error:internal"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
63
111
|
/**
|
|
64
112
|
* Express error handling middleware that converts errors to RFC 9457 format
|
|
65
113
|
*
|
|
@@ -83,41 +131,17 @@ var DefaultErrorFormatter = class {
|
|
|
83
131
|
* ```
|
|
84
132
|
*/
|
|
85
133
|
function errorHandler(options = {}) {
|
|
86
|
-
const { formatters = [new DefaultErrorFormatter()], logErrors = true
|
|
134
|
+
const { formatters = [new DefaultErrorFormatter()], logErrors = true } = options;
|
|
87
135
|
return (error, req, res, next) => {
|
|
88
136
|
if (res.headersSent) {
|
|
89
137
|
next(error);
|
|
90
138
|
return;
|
|
91
139
|
}
|
|
92
|
-
if (logErrors)
|
|
93
|
-
|
|
94
|
-
error: error.message,
|
|
95
|
-
code: error.code,
|
|
96
|
-
statusCode: error.statusCode,
|
|
97
|
-
path: req.path,
|
|
98
|
-
method: req.method
|
|
99
|
-
});
|
|
100
|
-
else logger.error(error, {
|
|
101
|
-
path: req.path,
|
|
102
|
-
method: req.method,
|
|
103
|
-
query: req.query,
|
|
104
|
-
...Buffer.isBuffer(req.body) ? {
|
|
105
|
-
bodyContentType: req.headers["content-type"],
|
|
106
|
-
bodyLength: req.body.length
|
|
107
|
-
} : { body: req.body }
|
|
108
|
-
});
|
|
109
|
-
let problemDetails;
|
|
110
|
-
const formatter = formatters.find((f) => f.canFormat(error));
|
|
111
|
-
if (formatter) problemDetails = formatter.format(error);
|
|
112
|
-
else problemDetails = (0, _batkit_rfc9457.createExtendedProblemDetails)({
|
|
113
|
-
type: "error:internal",
|
|
114
|
-
title: "Internal Server Error",
|
|
115
|
-
status: 500,
|
|
116
|
-
detail: "An unexpected error occurred"
|
|
117
|
-
});
|
|
140
|
+
if (logErrors) logHandledError(error, req, options);
|
|
141
|
+
const problemDetails = buildProblemDetails(error, formatters);
|
|
118
142
|
if (error.stack) problemDetails.stack = error.stack.split("\n").map((line) => line.trim());
|
|
119
143
|
if (!problemDetails.instance) problemDetails.instance = req.path;
|
|
120
|
-
res.status(problemDetails.status ||
|
|
144
|
+
res.status(problemDetails.status || HttpStatus.INTERNAL_SERVER_ERROR).set("Content-Type", _batkit_rfc9457.PROBLEM_DETAILS_CONTENT_TYPE).json(problemDetails);
|
|
121
145
|
};
|
|
122
146
|
}
|
|
123
147
|
/**
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["LoggerFacade","PROBLEM_DETAILS_CONTENT_TYPE"],"sources":["../src/error-handler.ts","../src/log-context-middleware.ts"],"sourcesContent":["import { isAppError } from \"@batkit/errors\";\nimport { LoggerFacade } from \"@batkit/logger\";\nimport {\n type ExtendedProblemDetails,\n PROBLEM_DETAILS_CONTENT_TYPE,\n createExtendedProblemDetails,\n} from \"@batkit/rfc9457\";\nimport type { NextFunction, Request, Response } from \"express\";\n\nconst logger = LoggerFacade.getLogger(\"error-handler\");\n/**\n * Interface for custom error formatters\n */\nexport interface ErrorFormatter {\n /**\n * Check if this formatter can handle the error\n */\n canFormat(error: unknown): boolean;\n\n /**\n * Format the error as RFC 9457 Problem Details\n */\n format(error: unknown): ExtendedProblemDetails;\n}\n\n/**\n * Default error formatter that handles common error types\n */\nexport class DefaultErrorFormatter implements ErrorFormatter {\n canFormat(error: unknown): boolean {\n return (\n isAppError(error) ||\n error instanceof Error ||\n this.isAxiosError(error) ||\n this.isZodError(error)\n );\n }\n\n format(error: unknown): ExtendedProblemDetails {\n // Handle AppError (from @batkit/errors)\n if (isAppError(error)) {\n return error.toRFC9457();\n }\n\n // Handle Axios errors\n if (this.isAxiosError(error)) {\n return this.formatAxiosError(error as Record<string, unknown>);\n }\n\n // Handle Zod validation errors\n if (this.isZodError(error)) {\n return this.formatZodError(error as Record<string, unknown>);\n }\n\n // Handle generic Error\n if (error instanceof Error) {\n return createExtendedProblemDetails({\n type: \"error:internal\",\n title: \"Internal Server Error\",\n status: 500,\n detail: error.message,\n });\n }\n\n // Unknown error type\n return createExtendedProblemDetails({\n type: \"error:unknown\",\n title: \"Unknown Error\",\n status: 500,\n detail: \"An unknown error occurred\",\n });\n }\n\n private isAxiosError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"isAxiosError\" in error &&\n error.isAxiosError === true\n );\n }\n\n private isZodError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"issues\" in error &&\n Array.isArray((error as Record<string, unknown>).issues)\n );\n }\n\n private formatAxiosError(error: Record<string, unknown>): ExtendedProblemDetails {\n const status = ((error.response as Record<string, unknown>)?.status as number) || 502;\n return createExtendedProblemDetails({\n type: \"error:upstream\",\n title: \"Upstream Service Error\",\n status,\n detail: error.message as string,\n upstreamUrl: (error.config as Record<string, unknown>)?.url as string | undefined,\n upstreamMethod: ((error.config as Record<string, unknown>)?.method as string)?.toUpperCase(),\n upstreamStatus: (error.response as Record<string, unknown>)?.status as number | undefined,\n });\n }\n\n private formatZodError(error: Record<string, unknown>): ExtendedProblemDetails {\n const validationErrors = (error.issues as Array<Record<string, unknown>>).map((issue) => ({\n field: (issue.path as unknown[]).join(\".\"),\n message: issue.message as string,\n code: issue.code as string,\n }));\n\n return createExtendedProblemDetails({\n type: \"error:validation\",\n title: \"Validation Error\",\n status: 422,\n detail: \"Request validation failed\",\n validationErrors,\n });\n }\n}\n\n/**\n * Options for error handler middleware\n */\nexport interface ErrorHandlerOptions {\n /**\n * Custom error formatters (in order of precedence)\n */\n formatters?: ErrorFormatter[];\n\n /**\n * Whether to log errors (default: true)\n */\n logErrors?: boolean;\n\n /**\n * Custom error logger function\n */\n onError?: (error: unknown, req: Request) => void;\n}\n\n/**\n * Express error handling middleware that converts errors to RFC 9457 format\n *\n * @param options - Configuration options\n * @returns Express error middleware function\n *\n * @example\n * ```ts\n * import { errorHandler } from '@batkit/express-middleware';\n * import { NotFoundError } from '@batkit/errors';\n * import express from 'express';\n *\n * const app = express();\n *\n * app.get('/users/:id', (req, res) => {\n * throw new NotFoundError('User', req.params.id);\n * });\n *\n * // Must be added AFTER all routes\n * app.use(errorHandler());\n * ```\n */\nexport function errorHandler(\n options: ErrorHandlerOptions = {},\n): (error: Error, req: Request, res: Response, next: NextFunction) => void {\n const { formatters = [new DefaultErrorFormatter()], logErrors = true, onError } = options;\n\n return (error: Error, req: Request, res: Response, next: NextFunction): void => {\n // If headers already sent, delegate to default Express error handler\n if (res.headersSent) {\n next(error);\n return;\n }\n\n // Log the error\n if (logErrors) {\n if (onError) {\n onError(error, req);\n } else {\n if (isAppError(error) && error.isOperational) {\n logger.warn(\"Operational error occurred\", {\n error: error.message,\n code: error.code,\n statusCode: error.statusCode,\n path: req.path,\n method: req.method,\n });\n } else {\n logger.error(error, {\n path: req.path,\n method: req.method,\n query: req.query,\n ...(Buffer.isBuffer(req.body)\n ? { bodyContentType: req.headers[\"content-type\"], bodyLength: req.body.length }\n : { body: req.body }),\n });\n }\n }\n }\n\n // Format the error\n let problemDetails: ExtendedProblemDetails;\n\n const formatter = formatters.find((f) => f.canFormat(error));\n if (formatter) {\n problemDetails = formatter.format(error);\n } else {\n // Fallback to generic error\n problemDetails = createExtendedProblemDetails({\n type: \"error:internal\",\n title: \"Internal Server Error\",\n status: 500,\n detail: \"An unexpected error occurred\",\n });\n }\n\n // Add stack trace\n if (error.stack) {\n problemDetails.stack = error.stack.split(\"\\n\").map((line) => line.trim());\n }\n\n // Add instance (request path)\n if (!problemDetails.instance) {\n problemDetails.instance = req.path;\n }\n\n // Send RFC 9457 response\n res\n .status(problemDetails.status || 500)\n .set(\"Content-Type\", PROBLEM_DETAILS_CONTENT_TYPE)\n .json(problemDetails);\n };\n}\n\n/**\n * Express middleware to catch async errors\n * Wraps async route handlers to ensure errors are passed to error middleware\n *\n * @param fn - Async route handler\n * @returns Wrapped route handler\n *\n * @example\n * ```ts\n * import { asyncHandler } from '@batkit/express-middleware';\n *\n * app.get('/users/:id', asyncHandler(async (req, res) => {\n * const user = await db.users.findById(req.params.id);\n * if (!user) {\n * throw new NotFoundError('User', req.params.id);\n * }\n * res.json(user);\n * }));\n * ```\n */\nexport function asyncHandler(\n fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>,\n): (req: Request, res: Response, next: NextFunction) => void {\n return (req: Request, res: Response, next: NextFunction): void => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n","import type { LogValue } from \"@batkit/logger\";\nimport { runWithContext } from \"@batkit/logger/async-local\";\nimport type { NextFunction, Request, Response } from \"express\";\n\nexport type LogContextInitializer = (req: Request) => Record<string, LogValue>;\n\nexport interface LogContextMiddlewareOptions {\n /**\n * Build initial async-local log fields for this HTTP request.\n * @default () => ({})\n */\n initialContext?: LogContextInitializer;\n}\n\n/**\n * Runs each request inside {@link runWithContext} so later code can use\n * {@link mergeLogContext} and a {@link ContextualLoggerProvider} without\n * depending on Express (`req`) deep in the stack.\n *\n * Mount this **early** in the middleware chain. Use synchronous `next()` inside\n * the callback; avoid `async` middleware that calls `next` after an `await`\n * without keeping the rest of the work in the same async context.\n */\nexport function logContextMiddleware(\n options: LogContextMiddlewareOptions = {},\n): (req: Request, res: Response, next: NextFunction) => void {\n const initialContext = options.initialContext ?? (() => ({}));\n\n return (req: Request, _res: Response, next: NextFunction) => {\n runWithContext(initialContext(req), () => next());\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["LoggerFacade","PROBLEM_DETAILS_CONTENT_TYPE"],"sources":["../src/error-handler.ts","../src/log-context-middleware.ts"],"sourcesContent":["import { isAppError } from \"@batkit/errors\";\nimport { LoggerFacade } from \"@batkit/logger\";\nimport {\n type ExtendedProblemDetails,\n PROBLEM_DETAILS_CONTENT_TYPE,\n createExtendedProblemDetails,\n} from \"@batkit/rfc9457\";\nimport type { NextFunction, Request, Response } from \"express\";\n\nconst logger = LoggerFacade.getLogger(\"error-handler\");\n\nconst HttpStatus = {\n BAD_GATEWAY: 502,\n INTERNAL_SERVER_ERROR: 500,\n UNPROCESSABLE_ENTITY: 422,\n} as const;\n/**\n * Interface for custom error formatters\n */\nexport interface ErrorFormatter {\n /**\n * Check if this formatter can handle the error\n */\n canFormat(error: unknown): boolean;\n\n /**\n * Format the error as RFC 9457 Problem Details\n */\n format(error: unknown): ExtendedProblemDetails;\n}\n\n/**\n * Default error formatter that handles common error types\n */\nexport class DefaultErrorFormatter implements ErrorFormatter {\n canFormat(error: unknown): boolean {\n return (\n isAppError(error) ||\n error instanceof Error ||\n this.isAxiosError(error) ||\n this.isZodError(error)\n );\n }\n\n format(error: unknown): ExtendedProblemDetails {\n // Handle AppError (from @batkit/errors)\n if (isAppError(error)) {\n return error.toRFC9457();\n }\n\n // Handle Axios errors\n if (this.isAxiosError(error)) {\n return this.formatAxiosError(error as Record<string, unknown>);\n }\n\n // Handle Zod validation errors\n if (this.isZodError(error)) {\n return this.formatZodError(error as Record<string, unknown>);\n }\n\n // Handle generic Error\n if (error instanceof Error) {\n return createExtendedProblemDetails({\n detail: error.message,\n status: HttpStatus.INTERNAL_SERVER_ERROR,\n title: \"Internal Server Error\",\n type: \"error:internal\",\n });\n }\n\n // Unknown error type\n return createExtendedProblemDetails({\n detail: \"An unknown error occurred\",\n status: HttpStatus.INTERNAL_SERVER_ERROR,\n title: \"Unknown Error\",\n type: \"error:unknown\",\n });\n }\n\n private isAxiosError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"isAxiosError\" in error &&\n error.isAxiosError === true\n );\n }\n\n private isZodError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"issues\" in error &&\n Array.isArray((error as Record<string, unknown>).issues)\n );\n }\n\n private formatAxiosError(error: Record<string, unknown>): ExtendedProblemDetails {\n const status =\n ((error.response as Record<string, unknown>)?.status as number) || HttpStatus.BAD_GATEWAY;\n return createExtendedProblemDetails({\n detail: error.message as string,\n status,\n title: \"Upstream Service Error\",\n type: \"error:upstream\",\n upstreamMethod: ((error.config as Record<string, unknown>)?.method as string)?.toUpperCase(),\n upstreamStatus: (error.response as Record<string, unknown>)?.status as number | undefined,\n upstreamUrl: (error.config as Record<string, unknown>)?.url as string | undefined,\n });\n }\n\n private formatZodError(error: Record<string, unknown>): ExtendedProblemDetails {\n const validationErrors = (error.issues as Record<string, unknown>[]).map((issue) => ({\n code: issue.code as string,\n field: (issue.path as unknown[]).join(\".\"),\n message: issue.message as string,\n }));\n\n return createExtendedProblemDetails({\n detail: \"Request validation failed\",\n status: HttpStatus.UNPROCESSABLE_ENTITY,\n title: \"Validation Error\",\n type: \"error:validation\",\n validationErrors,\n });\n }\n}\n\n/**\n * Options for error handler middleware\n */\nexport interface ErrorHandlerOptions {\n /**\n * Custom error formatters (in order of precedence)\n */\n formatters?: ErrorFormatter[];\n\n /**\n * Whether to log errors (default: true)\n */\n logErrors?: boolean;\n\n /**\n * Custom error logger function\n */\n onError?: (error: unknown, req: Request) => void;\n}\n\nfunction logHandledError(error: Error, req: Request, options: ErrorHandlerOptions): void {\n const { onError } = options;\n if (onError) {\n onError(error, req);\n return;\n }\n\n if (isAppError(error) && error.isOperational) {\n logger.warn(\"Operational error occurred\", {\n code: error.code,\n error: error.message,\n method: req.method,\n path: req.path,\n statusCode: error.statusCode,\n });\n return;\n }\n\n const contentType = req.headers?.[\"content-type\"];\n const isJsonContentType =\n typeof contentType === \"string\" && /^application\\/(?:[^;]+\\+)?json/iu.test(contentType);\n const isBinaryBody =\n Buffer.isBuffer(req.body) || (contentType !== undefined && !isJsonContentType);\n\n let bodyLength: number | undefined;\n if (Buffer.isBuffer(req.body)) {\n bodyLength = req.body.length;\n } else if (typeof req.body === \"string\") {\n bodyLength = Buffer.byteLength(req.body);\n }\n\n logger.error(error, {\n method: req.method,\n path: req.path,\n query: req.query,\n ...(isBinaryBody ? { bodyContentType: contentType, bodyLength } : { body: req.body }),\n });\n}\n\nfunction buildProblemDetails(error: Error, formatters: ErrorFormatter[]): ExtendedProblemDetails {\n const formatter = formatters.find((candidate) => candidate.canFormat(error));\n return formatter\n ? formatter.format(error)\n : createExtendedProblemDetails({\n detail: \"An unexpected error occurred\",\n status: HttpStatus.INTERNAL_SERVER_ERROR,\n title: \"Internal Server Error\",\n type: \"error:internal\",\n });\n}\n\n/**\n * Express error handling middleware that converts errors to RFC 9457 format\n *\n * @param options - Configuration options\n * @returns Express error middleware function\n *\n * @example\n * ```ts\n * import { errorHandler } from '@batkit/express-middleware';\n * import { NotFoundError } from '@batkit/errors';\n * import express from 'express';\n *\n * const app = express();\n *\n * app.get('/users/:id', (req, res) => {\n * throw new NotFoundError('User', req.params.id);\n * });\n *\n * // Must be added AFTER all routes\n * app.use(errorHandler());\n * ```\n */\nexport function errorHandler(\n options: ErrorHandlerOptions = {},\n // oxlint-disable-next-line max-params -- Express detects error-handling middleware by function arity; this signature must stay exactly (error, req, res, next).\n): (error: Error, req: Request, res: Response, next: NextFunction) => void {\n const { formatters = [new DefaultErrorFormatter()], logErrors = true } = options;\n\n // oxlint-disable-next-line max-params -- see above, Express requires this exact arity.\n return (error: Error, req: Request, res: Response, next: NextFunction): void => {\n // If headers already sent, delegate to default Express error handler\n if (res.headersSent) {\n next(error);\n return;\n }\n\n if (logErrors) {\n logHandledError(error, req, options);\n }\n\n const problemDetails = buildProblemDetails(error, formatters);\n\n // Add stack trace\n if (error.stack) {\n problemDetails.stack = error.stack.split(\"\\n\").map((line) => line.trim());\n }\n\n // Add instance (request path)\n if (!problemDetails.instance) {\n problemDetails.instance = req.path;\n }\n\n // Send RFC 9457 response\n res\n .status(problemDetails.status || HttpStatus.INTERNAL_SERVER_ERROR)\n .set(\"Content-Type\", PROBLEM_DETAILS_CONTENT_TYPE)\n .json(problemDetails);\n };\n}\n\n/**\n * Express middleware to catch async errors\n * Wraps async route handlers to ensure errors are passed to error middleware\n *\n * @param fn - Async route handler\n * @returns Wrapped route handler\n *\n * @example\n * ```ts\n * import { asyncHandler } from '@batkit/express-middleware';\n *\n * app.get('/users/:id', asyncHandler(async (req, res) => {\n * const user = await db.users.findById(req.params.id);\n * if (!user) {\n * throw new NotFoundError('User', req.params.id);\n * }\n * res.json(user);\n * }));\n * ```\n */\nexport function asyncHandler(\n fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>,\n): (req: Request, res: Response, next: NextFunction) => void {\n return (req: Request, res: Response, next: NextFunction): void => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n","import type { LogValue } from \"@batkit/logger\";\nimport { runWithContext } from \"@batkit/logger/async-local\";\nimport type { NextFunction, Request, Response } from \"express\";\n\nexport type LogContextInitializer = (req: Request) => Record<string, LogValue>;\n\nexport interface LogContextMiddlewareOptions {\n /**\n * Build initial async-local log fields for this HTTP request.\n * @default () => ({})\n */\n initialContext?: LogContextInitializer;\n}\n\n/**\n * Runs each request inside {@link runWithContext} so later code can use\n * {@link mergeLogContext} and a {@link ContextualLoggerProvider} without\n * depending on Express (`req`) deep in the stack.\n *\n * Mount this **early** in the middleware chain. Use synchronous `next()` inside\n * the callback; avoid `async` middleware that calls `next` after an `await`\n * without keeping the rest of the work in the same async context.\n */\nexport function logContextMiddleware(\n options: LogContextMiddlewareOptions = {},\n): (req: Request, res: Response, next: NextFunction) => void {\n const initialContext = options.initialContext ?? (() => ({}));\n\n return (req: Request, _res: Response, next: NextFunction) => {\n runWithContext(initialContext(req), () => next());\n };\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/error-handler.ts","../src/log-context-middleware.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/error-handler.ts","../src/log-context-middleware.ts"],"mappings":";;;;;UAmBiB,cAAA;EAIf,SAAA,CAAU,KAAA;EAKV,MAAA,CAAO,KAAA,YAAiB,sBAAsB;AAAA;AAAA,cAMnC,qBAAA,YAAiC,cAAc;EAC1D,SAAA,CAAU,KAAA;EASV,MAAA,CAAO,KAAA,YAAiB,sBAAA;EAAA,QAmChB,YAAA;EAAA,QASA,UAAA;EAAA,QASA,gBAAA;EAAA,QAcA,cAAA;AAAA;AAAA,UAoBO,mBAAA;EAIf,UAAA,GAAa,cAAA;EAKb,SAAA;EAKA,OAAA,IAAW,KAAA,WAAgB,GAAA,EAAK,OAAO;AAAA;AAAA,iBA4EzB,YAAA,CACd,OAAA,GAAS,mBAAA,IAEP,KAAA,EAAO,KAAA,EAAO,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;AAAA,iBAuDrC,YAAA,CACd,EAAA,GAAK,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA,KAAiB,OAAA,aACvD,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;;;KCrR3B,qBAAA,IAAyB,GAAA,EAAK,OAAA,KAAY,MAAA,SAAe,QAAA;AAAA,UAEpD,2BAAA;EAKf,cAAA,GAAiB,qBAAqB;AAAA;AAAA,iBAYxB,oBAAA,CACd,OAAA,GAAS,2BAAA,IACP,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@batkit/express-middleware",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Express middleware for error handling and request context (Node.js only)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"error-handling",
|
|
@@ -46,11 +46,12 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@batkit/errors": "0.1.
|
|
50
|
-
"@batkit/logger": "0.1.
|
|
51
|
-
"@batkit/rfc9457": "0.1.
|
|
49
|
+
"@batkit/errors": "0.1.3",
|
|
50
|
+
"@batkit/logger": "0.1.2",
|
|
51
|
+
"@batkit/rfc9457": "0.1.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@total-typescript/shoehorn": "^0.1.2",
|
|
54
55
|
"@types/express": "^5.0.0",
|
|
55
56
|
"express": "^4.21.2",
|
|
56
57
|
"typescript": "^5.7.2",
|