@vsaas/error-handler 10.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.md ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) IBM Corp. 2016,2017. All Rights Reserved.
2
+ Node module: strong-error-handler
3
+ This project is licensed under the MIT License, full text below.
4
+
5
+ ---
6
+
7
+ MIT license
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in
17
+ all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @vsaas/error-handler
2
+
3
+ HTTP error writer for Express and LoopBack 3 style applications.
4
+
5
+ `@vsaas/error-handler` is a fork of `strong-error-handler`. The goal of this fork is to stay compatible with the original package for the common LoopBack 3 / Strong Remoting use cases, while making the codebase smaller and easier to maintain.
6
+
7
+ Compared with the upstream package, this fork intentionally removes a number of legacy concerns:
8
+
9
+ - TypeScript source with build output in `dist/`
10
+ - English-only messages
11
+ - No i18n catalogs or `strong-globalize`
12
+ - Several legacy files and dependencies removed
13
+ - Modern tooling with `tsdown`, `vitest`, and `oxlint`
14
+
15
+ The public behavior is still meant to be familiar for applications that already depended on `strong-error-handler`, but the internals were simplified on purpose to reduce maintenance overhead.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @vsaas/error-handler
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Express middleware
26
+
27
+ ```js
28
+ const express = require('express');
29
+ const errorHandler = require('@vsaas/error-handler');
30
+
31
+ const app = express();
32
+
33
+ app.use(
34
+ errorHandler({
35
+ debug: process.env.NODE_ENV !== 'production',
36
+ log: true,
37
+ }),
38
+ );
39
+ ```
40
+
41
+ ### Direct response writer
42
+
43
+ ```js
44
+ const errorHandler = require('@vsaas/error-handler');
45
+
46
+ errorHandler.writeErrorToResponse(new Error('something went wrong'), req, res, {
47
+ debug: false,
48
+ });
49
+ ```
50
+
51
+ ## Response formats
52
+
53
+ The handler supports:
54
+
55
+ - `application/json` / `json`
56
+ - `text/html` / `html`
57
+ - `text/xml` / `xml`
58
+
59
+ Content type is negotiated from the request `Accept` header. You can also override it with the `_format` query parameter.
60
+
61
+ ## Options
62
+
63
+ | Option | Default | Description |
64
+ | ---------------------- | --------- | --------------------------------------------------------------------------- |
65
+ | `debug` | `false` | Include full error details and stack traces in responses. |
66
+ | `log` | `true` | Log errors to `console.error`. |
67
+ | `safeFields` | `[]` | Extra error properties allowed through for safe responses. |
68
+ | `defaultType` | `"json"` | Fallback response type when negotiation does not resolve one. |
69
+ | `rootProperty` | `"error"` | Root property for JSON/XML responses. Use `false` to omit the JSON wrapper. |
70
+ | `negotiateContentType` | `true` | When `false`, always use `defaultType` unless `_format` overrides it. |
71
+
72
+ ## Behavior
73
+
74
+ - `4xx` responses keep safe client-facing fields such as `message`, `details`, and `code`.
75
+ - `5xx` responses are sanitized by default to avoid leaking internals.
76
+ - When `debug: true`, the full error payload is returned.
77
+
78
+ ## Development
79
+
80
+ ```bash
81
+ npm run build
82
+ npm run typecheck
83
+ npm run lint
84
+ npm test
85
+ ```
86
+
87
+ ## License
88
+
89
+ MIT. See [LICENSE.md](./LICENSE.md).
@@ -0,0 +1,36 @@
1
+ //#region src/types.d.ts
2
+ interface RequestLike {
3
+ headers?: Record<string, string | string[] | undefined>;
4
+ method?: string;
5
+ query?: Record<string, unknown>;
6
+ socket?: {
7
+ destroy?: () => void;
8
+ };
9
+ url?: string;
10
+ }
11
+ interface ResponseLike {
12
+ headersSent?: boolean;
13
+ statusCode: number;
14
+ end: (content?: string, encoding?: BufferEncoding) => void;
15
+ setHeader: (name: string, value: string) => void;
16
+ }
17
+ type ErrorHandlerNext = (error?: unknown) => void;
18
+ interface ErrorWriterOptions {
19
+ debug?: boolean;
20
+ defaultType?: string;
21
+ negotiateContentType?: boolean;
22
+ rootProperty?: string | false;
23
+ safeFields?: string | string[];
24
+ }
25
+ interface ErrorHandlerOptions extends ErrorWriterOptions {
26
+ log?: boolean;
27
+ }
28
+ type StrongErrorHandler = (error: unknown, req: RequestLike, res: ResponseLike, next?: ErrorHandlerNext) => void;
29
+ interface StrongErrorHandlerFactory {
30
+ (options?: ErrorHandlerOptions): StrongErrorHandler;
31
+ writeErrorToResponse: (error: unknown, req: RequestLike, res: ResponseLike, options?: ErrorWriterOptions) => void;
32
+ }
33
+ //#endregion
34
+ //#region src/index.d.ts
35
+ declare const strongErrorHandler: StrongErrorHandlerFactory;
36
+ export = strongErrorHandler;
package/dist/index.js ADDED
@@ -0,0 +1,279 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let debug = require("debug");
24
+ debug = __toESM(debug);
25
+ let node_http = require("node:http");
26
+ let node_util = require("node:util");
27
+ node_util = __toESM(node_util);
28
+ let accepts = require("accepts");
29
+ accepts = __toESM(accepts);
30
+ let node_fs = require("node:fs");
31
+ node_fs = __toESM(node_fs);
32
+ let node_path = require("node:path");
33
+ node_path = __toESM(node_path);
34
+ let handlebars = require("handlebars");
35
+ handlebars = __toESM(handlebars);
36
+ //#region src/lib/clone.ts
37
+ function cloneAllProperties(data, error) {
38
+ data.name = error.name;
39
+ data.message = error.message;
40
+ for (const key in error) {
41
+ if (key in data) continue;
42
+ data[key] = error[key];
43
+ }
44
+ data.stack = error.stack;
45
+ }
46
+ //#endregion
47
+ //#region src/lib/data-builder.ts
48
+ function buildResponseData(error, options = {}) {
49
+ if (Array.isArray(error)) return serializeArrayOfErrors(error, options);
50
+ const normalizedError = normalizeError(error);
51
+ const data = { statusCode: resolveStatusCode(normalizedError) };
52
+ if (options.debug) {
53
+ cloneAllProperties(data, normalizedError);
54
+ return data;
55
+ }
56
+ if (data.statusCode >= 400 && data.statusCode <= 499) fillClientError(data, normalizedError);
57
+ else fillServerError(data, normalizedError);
58
+ fillSafeFields(data, normalizedError, options.safeFields);
59
+ return data;
60
+ }
61
+ function serializeArrayOfErrors(errors, options) {
62
+ const details = new Array(errors.length);
63
+ for (let index = 0; index < errors.length; index += 1) details[index] = buildResponseData(errors[index], options);
64
+ return {
65
+ statusCode: 500,
66
+ message: "Failed with multiple errors, see `details` for more information.",
67
+ details
68
+ };
69
+ }
70
+ function normalizeError(error) {
71
+ if (typeof error === "object" && error !== null) return error;
72
+ return {
73
+ statusCode: 500,
74
+ message: String(error)
75
+ };
76
+ }
77
+ function resolveStatusCode(error) {
78
+ const statusCode = error.statusCode || error.status;
79
+ if (!statusCode || statusCode < 400) return 500;
80
+ return statusCode;
81
+ }
82
+ function fillClientError(data, error) {
83
+ data.name = error.name;
84
+ data.message = error.message;
85
+ data.code = error.code;
86
+ data.details = error.details;
87
+ }
88
+ function fillServerError(data, error) {
89
+ if (error.expose) {
90
+ data.name = node_http.STATUS_CODES[data.statusCode] || "Unknown Error";
91
+ data.message = error.message;
92
+ } else data.message = node_http.STATUS_CODES[data.statusCode] || "Unknown Error";
93
+ }
94
+ function fillSafeFields(data, error, safeFields) {
95
+ if (!safeFields) return;
96
+ const fields = Array.isArray(safeFields) ? safeFields : [safeFields];
97
+ for (let index = 0; index < fields.length; index += 1) {
98
+ const field = fields[index];
99
+ if (error[field] !== void 0) data[field] = error[field];
100
+ }
101
+ }
102
+ //#endregion
103
+ //#region src/lib/logger.ts
104
+ function logToConsole(req, error) {
105
+ if (!Array.isArray(error)) {
106
+ console.error("Request %s %s failed: %s", req.method, req.url, formatError(error));
107
+ return;
108
+ }
109
+ const errors = error.map(formatError).join("\n");
110
+ console.error("Request %s %s failed with multiple errors:\n%s", req.method, req.url, errors);
111
+ }
112
+ function formatError(error) {
113
+ if (typeof error === "object" && error !== null && "stack" in error) return node_util.default.format("%s", error.stack || error);
114
+ return node_util.default.format("%s", error);
115
+ }
116
+ //#endregion
117
+ //#region src/lib/send-html.ts
118
+ const assetDir = node_path.default.resolve(__dirname, "../views");
119
+ const standardProps = new Set([
120
+ "name",
121
+ "statusCode",
122
+ "message",
123
+ "stack"
124
+ ]);
125
+ const stylesPartial = node_fs.default.readFileSync(node_path.default.join(assetDir, "style.hbs"), "utf8");
126
+ const defaultTemplate = handlebars.default.compile(node_fs.default.readFileSync(node_path.default.join(assetDir, "default-error.hbs"), "utf8"));
127
+ handlebars.default.registerHelper("partial", function partial(name) {
128
+ if (name === "style") return stylesPartial;
129
+ return "";
130
+ });
131
+ handlebars.default.registerHelper("standardProps", function standardPropsHelper(property, options) {
132
+ if (!standardProps.has(property)) return options.fn(this);
133
+ return options.inverse(this);
134
+ });
135
+ function sendHtml(res, data, options = {}) {
136
+ const filteredData = {};
137
+ for (const key in data) {
138
+ const value = data[key];
139
+ if (value !== void 0 && value !== null || standardProps.has(key)) filteredData[key] = value;
140
+ }
141
+ const body = defaultTemplate({
142
+ options,
143
+ data: filteredData
144
+ });
145
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
146
+ res.end(body, "utf-8");
147
+ }
148
+ //#endregion
149
+ //#region src/lib/safe-json-stringify.ts
150
+ const circularMarker = "[Circular]";
151
+ function safeJsonStringify(value) {
152
+ try {
153
+ return JSON.stringify(value);
154
+ } catch (error) {
155
+ if (!isCircularJsonStringifyError(error)) throw error;
156
+ }
157
+ return JSON.stringify(value, createCircularReplacer());
158
+ }
159
+ function createCircularReplacer() {
160
+ const ancestors = [];
161
+ const activeAncestors = /* @__PURE__ */ new WeakSet();
162
+ return function circularReplacer(_key, value) {
163
+ if (typeof value !== "object" || value === null) return value;
164
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
165
+ const ancestor = ancestors.pop();
166
+ if (ancestor) activeAncestors.delete(ancestor);
167
+ }
168
+ if (activeAncestors.has(value)) return circularMarker;
169
+ ancestors.push(value);
170
+ activeAncestors.add(value);
171
+ return value;
172
+ };
173
+ }
174
+ function isCircularJsonStringifyError(error) {
175
+ return error instanceof TypeError && (error.message.includes("circular") || error.message.includes("Circular"));
176
+ }
177
+ //#endregion
178
+ //#region src/lib/send-json.ts
179
+ function sendJson(res, data, options = {}) {
180
+ const content = options.rootProperty === false ? safeJsonStringify(data) : safeJsonStringify({ [options.rootProperty || "error"]: data });
181
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
182
+ res.end(content, "utf-8");
183
+ }
184
+ //#endregion
185
+ //#region src/lib/send-xml.ts
186
+ const js2xmlparser = require("js2xmlparser");
187
+ function sendXml(res, data, options = {}) {
188
+ const root = options.rootProperty || "error";
189
+ const content = js2xmlparser.parse(root, data);
190
+ res.setHeader("Content-Type", "text/xml; charset=utf-8");
191
+ res.end(content, "utf-8");
192
+ }
193
+ //#endregion
194
+ //#region src/lib/content-negotiation.ts
195
+ const debug$2 = (0, debug.default)("strong-error-handler:http-response");
196
+ const supportedTypes = [
197
+ "application/json",
198
+ "json",
199
+ "text/html",
200
+ "html",
201
+ "text/xml",
202
+ "xml"
203
+ ];
204
+ const supportedTypeSet = new Set(supportedTypes);
205
+ function negotiateContentProducer(req, logWarning, options = {}) {
206
+ let defaultType = "json";
207
+ if (typeof options.defaultType === "string" && supportedTypeSet.has(options.defaultType)) {
208
+ debug$2("Accepting options.defaultType `%s`", options.defaultType);
209
+ defaultType = options.defaultType;
210
+ } else if (options.defaultType) debug$2("defaultType: `%s` is not supported, falling back to defaultType: `%s`", options.defaultType, defaultType);
211
+ let contentType = (0, accepts.default)(req).types([...supportedTypes]) || defaultType;
212
+ if (options.negotiateContentType === false) if (typeof options.defaultType === "string" && supportedTypeSet.has(options.defaultType)) {
213
+ contentType = options.defaultType;
214
+ debug$2("Forcing options.defaultType `%s`", options.defaultType);
215
+ } else debug$2("contentType: `%s` is not supported, falling back to contentType: `%s`", options.defaultType, contentType);
216
+ const query = req.query || {};
217
+ if (typeof query._format === "string") if (supportedTypeSet.has(query._format)) contentType = query._format;
218
+ else logWarning(node_util.default.format("Response _format \"%s\" is not supported, used \"%s\" instead", query._format, defaultType));
219
+ debug$2("Content-negotiation: req.headers.accept: `%s` resolved as: `%s`", req.headers?.accept, contentType);
220
+ return resolveOperation(contentType);
221
+ }
222
+ function resolveOperation(contentType) {
223
+ switch (contentType) {
224
+ case "application/json":
225
+ case "json": return sendJson;
226
+ case "text/html":
227
+ case "html": return sendHtml;
228
+ case "text/xml":
229
+ case "xml": return sendXml;
230
+ default: return sendJson;
231
+ }
232
+ }
233
+ //#endregion
234
+ //#region src/lib/handler.ts
235
+ const debug$1 = (0, debug.default)("strong-error-handler");
236
+ function createStrongErrorHandler(options = {}) {
237
+ debug$1("Initializing with options %j", options);
238
+ const logError = options.log !== false ? logToConsole : noop;
239
+ return function strongErrorHandler(error, req, res) {
240
+ logError(req, error);
241
+ writeErrorToResponse(error, req, res, options);
242
+ };
243
+ }
244
+ function writeErrorToResponse(error, req, res, options = {}) {
245
+ debug$1("Handling %s", getDebugValue(error));
246
+ if (res.headersSent) {
247
+ debug$1("Response was already sent, closing the underlying connection");
248
+ req.socket?.destroy?.();
249
+ return;
250
+ }
251
+ const data = buildResponseData(coerceWritableError(error, res.statusCode), options);
252
+ debug$1("Response status %s data %j", data.statusCode, data);
253
+ res.setHeader("X-Content-Type-Options", "nosniff");
254
+ res.statusCode = data.statusCode;
255
+ negotiateContentProducer(req, warn, options)(res, data, options);
256
+ function warn(message) {
257
+ res.setHeader("X-Warning", message);
258
+ debug$1(message);
259
+ }
260
+ }
261
+ function noop() {}
262
+ function getDebugValue(error) {
263
+ if (typeof error === "object" && error !== null && "stack" in error) return error.stack || error;
264
+ return error;
265
+ }
266
+ function coerceWritableError(error, responseStatusCode) {
267
+ if (typeof error !== "object" || error === null) return {
268
+ statusCode: responseStatusCode >= 400 ? responseStatusCode : 500,
269
+ message: String(error)
270
+ };
271
+ const writableError = error;
272
+ if (!writableError.status && !writableError.statusCode && responseStatusCode >= 400) writableError.statusCode = responseStatusCode;
273
+ return writableError;
274
+ }
275
+ //#endregion
276
+ //#region src/index.ts
277
+ const strongErrorHandler = Object.assign(createStrongErrorHandler, { writeErrorToResponse });
278
+ module.exports = strongErrorHandler;
279
+ //#endregion
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@vsaas/error-handler",
3
+ "version": "10.0.0",
4
+ "description": "Error handler for use in development and production environments.",
5
+ "license": "MIT",
6
+ "author": "IBM Corp.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/xompass/strong-error-handler"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "views",
14
+ "README.md",
15
+ "LICENSE.md"
16
+ ],
17
+ "main": "dist/index.js",
18
+ "types": "dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "require": "./dist/index.js",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "registry": "https://registry.npmjs.org/"
30
+ },
31
+ "dependencies": {
32
+ "accepts": "^1.3.8",
33
+ "debug": "^4.4.3",
34
+ "handlebars": "^4.7.9",
35
+ "js2xmlparser": "^5.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "25.5.0",
39
+ "oxfmt": "0.42.0",
40
+ "oxlint": "1.57.0",
41
+ "supertest": "7.2.2",
42
+ "tsdown": "0.21.5",
43
+ "typescript": "6.0.2",
44
+ "vitest": "4.1.1"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "fmt": "oxfmt -c ../.oxfmtrc.json .",
52
+ "lint": "oxlint -c ../.oxlintrc.json src test",
53
+ "lint:fix": "oxlint -c ../.oxlintrc.json src test --fix",
54
+ "test:run": "vitest run",
55
+ "test": "pnpm run build && vitest run",
56
+ "typecheck": "tsc --noEmit -p tsconfig.json"
57
+ }
58
+ }
@@ -0,0 +1,26 @@
1
+ <html>
2
+ <head>
3
+ <meta charset='utf-8' />
4
+ <title>{{data.name}}{{#unless data.name}}{{data.message}}{{/unless}}</title>
5
+ <style>
6
+ {{partial 'style'}}
7
+ </style>
8
+ </head>
9
+ <body>
10
+ <div id='wrapper'>
11
+ <h1>{{data.name}}</h1>
12
+ <h2>
13
+ <em>{{data.statusCode}}</em>
14
+ {{data.message}}
15
+ </h2>
16
+ {{#each data}}
17
+ {{#standardProps @key}}
18
+ <div><b>{{@key}}</b>: {{this}}</div>
19
+ {{/standardProps}}
20
+ {{/each}}
21
+ {{#if data.stack}}
22
+ <pre id='stacktrace'>{{{data.stack}}}</pre>
23
+ {{/if}}
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,7 @@
1
+ * { margin: 0; padding: 0; outline: 0; } body { padding: 80px 100px; font: 13px
2
+ "Helvetica Neue", "Lucida Grande", "Arial"; background: #ECE9E9
3
+ -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ECE9E9)); background:
4
+ #ECE9E9 -moz-linear-gradient(top, #fff, #ECE9E9); background-repeat: no-repeat;
5
+ color: #555; -webkit-font-smoothing: antialiased; } h1, h2 { font-size: 22px;
6
+ color: #343434; } h1 em, h2 em { padding: 0 5px; font-weight: normal; } h1 {
7
+ font-size: 60px; } h2 { margin: 10px 0; } ul li { list-style: none; }