webspresso 0.0.90 → 0.0.91
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 +358 -8
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/middleware.js +9 -0
- package/core/compression/compressible.js +51 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +301 -0
- package/core/compression/negotiate.js +153 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/orm/complexity.js +120 -0
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +45 -10
- package/core/orm/model.js +2 -0
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +284 -0
- package/core/queue/adapters/memory.js +155 -0
- package/core/queue/adapters/redis.js +336 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +387 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/index.d.ts +829 -52
- package/index.js +103 -0
- package/package.json +22 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
- package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +90 -17
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +5 -1
- package/plugins/data-exchange/import.js +16 -2
- package/plugins/data-exchange/index.js +14 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +29 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +196 -21
- package/src/helpers.js +20 -2
- package/src/plugin-manager.js +124 -4
- package/src/server.js +304 -44
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +140 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +288 -0
- package/src/services/validator.js +281 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework Domain Exceptions
|
|
3
|
+
* Includes Validation, Configuration, Plugin, Router, Security, and Request errors.
|
|
4
|
+
* @module core/errors/domain
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { WebspressoError } = require('./base');
|
|
8
|
+
const { HttpError, NotFoundError } = require('./http');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validation Error
|
|
12
|
+
* Represents schema or business rule validation failures.
|
|
13
|
+
* Maps to HTTP 422 Unprocessable Entity by default.
|
|
14
|
+
*/
|
|
15
|
+
class ValidationError extends HttpError {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} [message='Validation failed']
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {Record<string, string[]|string>} [options.fields={}] - Field-specific validation error messages
|
|
20
|
+
* @param {number} [options.status=422]
|
|
21
|
+
* @param {string} [options.code='VALIDATION_ERROR']
|
|
22
|
+
* @param {unknown} [options.details]
|
|
23
|
+
* @param {unknown} [options.cause]
|
|
24
|
+
*/
|
|
25
|
+
constructor(message = 'Validation failed', options = {}) {
|
|
26
|
+
super(options.status || 422, message, {
|
|
27
|
+
code: options.code || 'VALIDATION_ERROR',
|
|
28
|
+
expose: true,
|
|
29
|
+
...options,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.fields = options.fields || {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configuration Error
|
|
38
|
+
* Represents framework, server, or application bootstrap misconfiguration.
|
|
39
|
+
*/
|
|
40
|
+
class ConfigurationError extends WebspressoError {
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} message
|
|
43
|
+
* @param {Object} [options]
|
|
44
|
+
*/
|
|
45
|
+
constructor(message, options = {}) {
|
|
46
|
+
super(message, {
|
|
47
|
+
code: options.code || 'CONFIGURATION_ERROR',
|
|
48
|
+
status: 500,
|
|
49
|
+
...options,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Plugin Error
|
|
56
|
+
* Represents plugin lifecycle, registration, or dependency failure.
|
|
57
|
+
*/
|
|
58
|
+
class PluginError extends WebspressoError {
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} message
|
|
61
|
+
* @param {Object} [options]
|
|
62
|
+
* @param {string} [options.plugin] - Name of the plugin that threw the error
|
|
63
|
+
*/
|
|
64
|
+
constructor(message, options = {}) {
|
|
65
|
+
super(message, {
|
|
66
|
+
code: options.code || 'PLUGIN_ERROR',
|
|
67
|
+
status: 500,
|
|
68
|
+
...options,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.plugin = options.plugin || null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Security Error
|
|
77
|
+
* Represents suspicious requests, invalid signatures, CSRF/cookie tampering.
|
|
78
|
+
* Maps to HTTP 400 Bad Request by default, with internal details masked.
|
|
79
|
+
*/
|
|
80
|
+
class SecurityError extends HttpError {
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} [message='Security violation']
|
|
83
|
+
* @param {Object} [options]
|
|
84
|
+
*/
|
|
85
|
+
constructor(message = 'Security violation', options = {}) {
|
|
86
|
+
super(options.status || 400, message, {
|
|
87
|
+
code: options.code || 'SECURITY_ERROR',
|
|
88
|
+
expose: false,
|
|
89
|
+
...options,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Base Request Transport Error
|
|
96
|
+
*/
|
|
97
|
+
class RequestError extends HttpError {
|
|
98
|
+
/**
|
|
99
|
+
* @param {number} status
|
|
100
|
+
* @param {string} message
|
|
101
|
+
* @param {Object} [options]
|
|
102
|
+
*/
|
|
103
|
+
constructor(status = 400, message = 'Request Error', options = {}) {
|
|
104
|
+
super(status, message, options);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Request Aborted Error
|
|
110
|
+
* Thrown when client connection is terminated/aborted before response completion.
|
|
111
|
+
*/
|
|
112
|
+
class RequestAbortedError extends RequestError {
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} [message='Request aborted by client']
|
|
115
|
+
* @param {Object} [options]
|
|
116
|
+
*/
|
|
117
|
+
constructor(message = 'Request aborted by client', options = {}) {
|
|
118
|
+
super(options.status || 499, message, {
|
|
119
|
+
code: options.code || 'REQUEST_ABORTED',
|
|
120
|
+
expose: false,
|
|
121
|
+
...options,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Base Router Error
|
|
128
|
+
*/
|
|
129
|
+
class RouterError extends WebspressoError {
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} message
|
|
132
|
+
* @param {Object} [options]
|
|
133
|
+
*/
|
|
134
|
+
constructor(message, options = {}) {
|
|
135
|
+
super(message, options);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Route Not Found Error
|
|
141
|
+
* Represents a URL that cannot be resolved to any page or route handler.
|
|
142
|
+
* Maps to HTTP 404.
|
|
143
|
+
*/
|
|
144
|
+
class RouteNotFoundError extends NotFoundError {
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} [pathOrMessage='Route not found']
|
|
147
|
+
* @param {Object} [options]
|
|
148
|
+
*/
|
|
149
|
+
constructor(pathOrMessage = 'Route not found', options = {}) {
|
|
150
|
+
const msg = typeof pathOrMessage === 'string' && pathOrMessage.startsWith('/')
|
|
151
|
+
? `Route not found: ${pathOrMessage}`
|
|
152
|
+
: pathOrMessage;
|
|
153
|
+
|
|
154
|
+
super(msg, {
|
|
155
|
+
code: options.code || 'ROUTE_NOT_FOUND',
|
|
156
|
+
...options,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (typeof pathOrMessage === 'string' && pathOrMessage.startsWith('/')) {
|
|
160
|
+
this.path = pathOrMessage;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Route Generation Error
|
|
167
|
+
* Developer error when generating a URL for a named route with missing or invalid parameters.
|
|
168
|
+
*/
|
|
169
|
+
class RouteGenerationError extends RouterError {
|
|
170
|
+
/**
|
|
171
|
+
* @param {string} message
|
|
172
|
+
* @param {Object} [options]
|
|
173
|
+
*/
|
|
174
|
+
constructor(message, options = {}) {
|
|
175
|
+
super(message, {
|
|
176
|
+
code: options.code || 'ROUTE_GENERATION_ERROR',
|
|
177
|
+
status: 500,
|
|
178
|
+
...options,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
ValidationError,
|
|
185
|
+
ConfigurationError,
|
|
186
|
+
PluginError,
|
|
187
|
+
SecurityError,
|
|
188
|
+
RequestError,
|
|
189
|
+
RequestAbortedError,
|
|
190
|
+
RouterError,
|
|
191
|
+
RouteNotFoundError,
|
|
192
|
+
RouteGenerationError,
|
|
193
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Exception Hierarchy
|
|
3
|
+
* @module core/errors/http
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { WebspressoError } = require('./base');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base HTTP Error
|
|
10
|
+
*/
|
|
11
|
+
class HttpError extends WebspressoError {
|
|
12
|
+
/**
|
|
13
|
+
* @param {number} status - HTTP status code
|
|
14
|
+
* @param {string} [message] - Error message
|
|
15
|
+
* @param {Object} [options]
|
|
16
|
+
* @param {string} [options.code] - Machine-readable error code
|
|
17
|
+
* @param {unknown} [options.details] - Additional contextual details
|
|
18
|
+
* @param {unknown} [options.cause] - Underlying root cause error
|
|
19
|
+
* @param {Record<string, string>} [options.headers] - Response headers to attach
|
|
20
|
+
* @param {boolean} [options.expose] - Whether to expose message to clients in production
|
|
21
|
+
*/
|
|
22
|
+
constructor(status = 500, message = 'Internal Server Error', options = {}) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
|
|
25
|
+
this.status = typeof status === 'number' ? status : 500;
|
|
26
|
+
this.headers = options.headers || {};
|
|
27
|
+
this.expose = options.expose !== undefined ? Boolean(options.expose) : this.status < 500;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 400 Bad Request
|
|
33
|
+
*/
|
|
34
|
+
class BadRequestError extends HttpError {
|
|
35
|
+
constructor(message = 'Bad Request', options = {}) {
|
|
36
|
+
super(400, message, { code: 'BAD_REQUEST', ...options });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 401 Unauthorized
|
|
42
|
+
*/
|
|
43
|
+
class UnauthorizedError extends HttpError {
|
|
44
|
+
constructor(message = 'Unauthorized', options = {}) {
|
|
45
|
+
super(401, message, { code: 'UNAUTHORIZED', ...options });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 403 Forbidden
|
|
51
|
+
*/
|
|
52
|
+
class ForbiddenError extends HttpError {
|
|
53
|
+
constructor(message = 'Forbidden', options = {}) {
|
|
54
|
+
super(403, message, { code: 'FORBIDDEN', ...options });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 404 Not Found
|
|
60
|
+
*/
|
|
61
|
+
class NotFoundError extends HttpError {
|
|
62
|
+
constructor(message = 'Not Found', options = {}) {
|
|
63
|
+
super(404, message, { code: 'NOT_FOUND', ...options });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 405 Method Not Allowed
|
|
69
|
+
*/
|
|
70
|
+
class MethodNotAllowedError extends HttpError {
|
|
71
|
+
constructor(message = 'Method Not Allowed', options = {}) {
|
|
72
|
+
super(405, message, { code: 'METHOD_NOT_ALLOWED', ...options });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 409 Conflict
|
|
78
|
+
*/
|
|
79
|
+
class ConflictError extends HttpError {
|
|
80
|
+
constructor(message = 'Conflict', options = {}) {
|
|
81
|
+
super(409, message, { code: 'CONFLICT', ...options });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 413 Payload Too Large
|
|
87
|
+
*/
|
|
88
|
+
class PayloadTooLargeError extends HttpError {
|
|
89
|
+
constructor(message = 'Payload Too Large', options = {}) {
|
|
90
|
+
super(413, message, { code: 'PAYLOAD_TOO_LARGE', ...options });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 415 Unsupported Media Type
|
|
96
|
+
*/
|
|
97
|
+
class UnsupportedMediaTypeError extends HttpError {
|
|
98
|
+
constructor(message = 'Unsupported Media Type', options = {}) {
|
|
99
|
+
super(415, message, { code: 'UNSUPPORTED_MEDIA_TYPE', ...options });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 422 Unprocessable Entity
|
|
105
|
+
*/
|
|
106
|
+
class UnprocessableEntityError extends HttpError {
|
|
107
|
+
constructor(message = 'Unprocessable Entity', options = {}) {
|
|
108
|
+
super(422, message, { code: 'UNPROCESSABLE_ENTITY', ...options });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 429 Too Many Requests
|
|
114
|
+
*/
|
|
115
|
+
class TooManyRequestsError extends HttpError {
|
|
116
|
+
constructor(message = 'Too Many Requests', options = {}) {
|
|
117
|
+
super(429, message, { code: 'TOO_MANY_REQUESTS', ...options });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
HttpError,
|
|
123
|
+
BadRequestError,
|
|
124
|
+
UnauthorizedError,
|
|
125
|
+
ForbiddenError,
|
|
126
|
+
NotFoundError,
|
|
127
|
+
MethodNotAllowedError,
|
|
128
|
+
ConflictError,
|
|
129
|
+
PayloadTooLargeError,
|
|
130
|
+
UnsupportedMediaTypeError,
|
|
131
|
+
UnprocessableEntityError,
|
|
132
|
+
TooManyRequestsError,
|
|
133
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Exceptions & Error Handling Module
|
|
3
|
+
* @module core/errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { WebspressoError } = require('./base');
|
|
7
|
+
const {
|
|
8
|
+
HttpError,
|
|
9
|
+
BadRequestError,
|
|
10
|
+
UnauthorizedError,
|
|
11
|
+
ForbiddenError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
MethodNotAllowedError,
|
|
14
|
+
ConflictError,
|
|
15
|
+
PayloadTooLargeError,
|
|
16
|
+
UnsupportedMediaTypeError,
|
|
17
|
+
UnprocessableEntityError,
|
|
18
|
+
TooManyRequestsError,
|
|
19
|
+
} = require('./http');
|
|
20
|
+
const {
|
|
21
|
+
ValidationError,
|
|
22
|
+
ConfigurationError,
|
|
23
|
+
PluginError,
|
|
24
|
+
SecurityError,
|
|
25
|
+
RequestError,
|
|
26
|
+
RequestAbortedError,
|
|
27
|
+
RouterError,
|
|
28
|
+
RouteNotFoundError,
|
|
29
|
+
RouteGenerationError,
|
|
30
|
+
} = require('./domain');
|
|
31
|
+
const {
|
|
32
|
+
getStatusTitle,
|
|
33
|
+
normalizeError,
|
|
34
|
+
toErrorResponseObject,
|
|
35
|
+
} = require('./normalize');
|
|
36
|
+
const {
|
|
37
|
+
preferJsonErrorResponse,
|
|
38
|
+
createCentralErrorHandler,
|
|
39
|
+
renderDefaultErrorHtml,
|
|
40
|
+
} = require('./middleware');
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
// Base
|
|
44
|
+
WebspressoError,
|
|
45
|
+
|
|
46
|
+
// HTTP Exceptions
|
|
47
|
+
HttpError,
|
|
48
|
+
BadRequestError,
|
|
49
|
+
UnauthorizedError,
|
|
50
|
+
ForbiddenError,
|
|
51
|
+
NotFoundError,
|
|
52
|
+
MethodNotAllowedError,
|
|
53
|
+
ConflictError,
|
|
54
|
+
PayloadTooLargeError,
|
|
55
|
+
UnsupportedMediaTypeError,
|
|
56
|
+
UnprocessableEntityError,
|
|
57
|
+
TooManyRequestsError,
|
|
58
|
+
|
|
59
|
+
// Domain Exceptions
|
|
60
|
+
ValidationError,
|
|
61
|
+
ConfigurationError,
|
|
62
|
+
PluginError,
|
|
63
|
+
SecurityError,
|
|
64
|
+
RequestError,
|
|
65
|
+
RequestAbortedError,
|
|
66
|
+
RouterError,
|
|
67
|
+
RouteNotFoundError,
|
|
68
|
+
RouteGenerationError,
|
|
69
|
+
|
|
70
|
+
// Utilities & Middleware
|
|
71
|
+
getStatusTitle,
|
|
72
|
+
normalizeError,
|
|
73
|
+
toErrorResponseObject,
|
|
74
|
+
preferJsonErrorResponse,
|
|
75
|
+
createCentralErrorHandler,
|
|
76
|
+
renderDefaultErrorHtml,
|
|
77
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central Error Boundary Middleware
|
|
3
|
+
* @module core/errors/middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { normalizeError, toErrorResponseObject } = require('./normalize');
|
|
7
|
+
const { RequestAbortedError } = require('./domain');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if the request prefers a JSON error response
|
|
11
|
+
* @param {import('express').Request} req
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
function preferJsonErrorResponse(req) {
|
|
15
|
+
if (req.path.startsWith('/api/') || req.path.startsWith('/_admin/api/')) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const accept = req.headers['accept'] || '';
|
|
19
|
+
if (accept.includes('application/json') && !accept.includes('text/html')) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return req.xhr || req.headers['x-requested-with'] === 'XMLHttpRequest';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default HTML error renderer fallback
|
|
27
|
+
* @param {import('./http').HttpError|Error} err
|
|
28
|
+
* @param {boolean} isDev
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function renderDefaultErrorHtml(err, isDev) {
|
|
32
|
+
const status = err.status || 500;
|
|
33
|
+
const message = (err.expose !== false || isDev) ? (err.message || 'Internal Server Error') : 'Internal Server Error';
|
|
34
|
+
const stackHtml = isDev && err.stack
|
|
35
|
+
? `<pre style="background:#1e293b;color:#f87171;padding:16px;border-radius:8px;overflow:auto;font-size:13px;line-height:1.5;">${escapeHtml(err.stack)}</pre>`
|
|
36
|
+
: '';
|
|
37
|
+
|
|
38
|
+
return `<!DOCTYPE html>
|
|
39
|
+
<html lang="en">
|
|
40
|
+
<head>
|
|
41
|
+
<meta charset="UTF-8">
|
|
42
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
43
|
+
<title>${status} - ${escapeHtml(message)}</title>
|
|
44
|
+
<style>
|
|
45
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #f8fafc; margin: 0; padding: 40px 20px; display: flex; justify-content: center; }
|
|
46
|
+
.card { background: #1e293b; padding: 32px; border-radius: 12px; max-width: 650px; width: 100%; border: 1px solid #334155; }
|
|
47
|
+
h1 { margin-top: 0; color: #ef4444; font-size: 24px; }
|
|
48
|
+
p { color: #cbd5e1; line-height: 1.6; }
|
|
49
|
+
a { color: #38bdf8; text-decoration: none; }
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="card">
|
|
54
|
+
<h1>Error ${status}</h1>
|
|
55
|
+
<p>${escapeHtml(message)}</p>
|
|
56
|
+
${stackHtml}
|
|
57
|
+
<p><a href="/">← Back to Home</a></p>
|
|
58
|
+
</div>
|
|
59
|
+
</body>
|
|
60
|
+
</html>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function escapeHtml(str) {
|
|
64
|
+
return String(str || '')
|
|
65
|
+
.replace(/&/g, '&')
|
|
66
|
+
.replace(/</g, '<')
|
|
67
|
+
.replace(/>/g, '>')
|
|
68
|
+
.replace(/"/g, '"')
|
|
69
|
+
.replace(/'/g, ''');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create Webspresso Central Error Handler Middleware
|
|
74
|
+
* @param {Object} [options]
|
|
75
|
+
* @param {boolean} [options.isDev=false]
|
|
76
|
+
* @param {Function} [options.getCustomHandler]
|
|
77
|
+
* @param {Function} [options.renderHtmlError]
|
|
78
|
+
* @returns {import('express').ErrorRequestHandler}
|
|
79
|
+
*/
|
|
80
|
+
function createCentralErrorHandler(options = {}) {
|
|
81
|
+
const isDev = Boolean(options.isDev);
|
|
82
|
+
const getCustomHandler = typeof options.getCustomHandler === 'function' ? options.getCustomHandler : () => null;
|
|
83
|
+
const renderHtmlError = typeof options.renderHtmlError === 'function' ? options.renderHtmlError : null;
|
|
84
|
+
|
|
85
|
+
return async function centralErrorHandler(err, req, res, next) {
|
|
86
|
+
if (res.headersSent) {
|
|
87
|
+
return next(err);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle Client Request Aborted
|
|
91
|
+
if (err instanceof RequestAbortedError || req.aborted || (req.socket && req.socket.destroyed)) {
|
|
92
|
+
// Do not attempt to write to closed socket
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Normalize error
|
|
97
|
+
const normalized = normalizeError(err, isDev);
|
|
98
|
+
const status = normalized.status || 500;
|
|
99
|
+
|
|
100
|
+
// Attach custom headers from HttpError if present
|
|
101
|
+
if (normalized.headers && typeof normalized.headers === 'object') {
|
|
102
|
+
for (const [key, value] of Object.entries(normalized.headers)) {
|
|
103
|
+
res.setHeader(key, value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for custom error handler registered via app.setErrorHandler(...)
|
|
108
|
+
const customHandler = getCustomHandler();
|
|
109
|
+
if (typeof customHandler === 'function') {
|
|
110
|
+
try {
|
|
111
|
+
const handled = await customHandler(err, req, res, next);
|
|
112
|
+
if (res.headersSent || handled === true) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
} catch (customErr) {
|
|
116
|
+
// Prevent infinite error loops by logging custom handler error and continuing with fallback
|
|
117
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
118
|
+
console.error('[webspresso] Error in custom error handler:', customErr);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Log errors based on severity
|
|
124
|
+
if (status >= 500) {
|
|
125
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
126
|
+
console.error('[webspresso] Server Error:', err);
|
|
127
|
+
}
|
|
128
|
+
} else if (isDev && status >= 400) {
|
|
129
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
130
|
+
console.warn(`[webspresso] HTTP ${status}:`, normalized.message);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
res.status(status);
|
|
135
|
+
|
|
136
|
+
// Format response (JSON vs HTML)
|
|
137
|
+
if (preferJsonErrorResponse(req)) {
|
|
138
|
+
return res.json(toErrorResponseObject(normalized, isDev));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (renderHtmlError) {
|
|
142
|
+
try {
|
|
143
|
+
const html = await renderHtmlError(normalized, req, res);
|
|
144
|
+
if (html && !res.headersSent) {
|
|
145
|
+
return res.send(html);
|
|
146
|
+
}
|
|
147
|
+
} catch (renderErr) {
|
|
148
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
149
|
+
console.error('[webspresso] Error rendering HTML error page:', renderErr);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return res.send(renderDefaultErrorHtml(normalized, isDev));
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
preferJsonErrorResponse,
|
|
160
|
+
createCentralErrorHandler,
|
|
161
|
+
renderDefaultErrorHtml,
|
|
162
|
+
};
|