@webpieces/http-server 0.4.399 → 0.4.401
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/package.json +5 -5
- package/src/ExpressWrapper.d.ts +62 -0
- package/src/ExpressWrapper.js +227 -0
- package/src/ExpressWrapper.js.map +1 -0
- package/src/WebpiecesExpressRouter.js +4 -4
- package/src/WebpiecesExpressRouter.js.map +1 -1
- package/src/WebpiecesMiddleware.d.ts +1 -61
- package/src/WebpiecesMiddleware.js +13 -226
- package/src/WebpiecesMiddleware.js.map +1 -1
- package/src/recorder/TestCaseRecorderImpl.js +3 -3
- package/src/recorder/TestCaseRecorderImpl.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.401",
|
|
4
4
|
"description": "WebPieces server with filter chain and dependency injection",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-context": "0.4.
|
|
26
|
-
"@webpieces/core-util": "0.4.
|
|
27
|
-
"@webpieces/gcp-identity": "0.4.
|
|
28
|
-
"@webpieces/http-routing": "0.4.
|
|
25
|
+
"@webpieces/core-context": "0.4.401",
|
|
26
|
+
"@webpieces/core-util": "0.4.401",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.401",
|
|
28
|
+
"@webpieces/http-routing": "0.4.401",
|
|
29
29
|
"cors": "2.8.5",
|
|
30
30
|
"express": "5.1.0",
|
|
31
31
|
"inversify": "7.10.4"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { RequestContextHeaders } from '@webpieces/core-context';
|
|
3
|
+
export declare class ExpressWrapper {
|
|
4
|
+
private clientMethod;
|
|
5
|
+
private path;
|
|
6
|
+
/** Owns the wire<->context transfer, both directions. Stateless framework singleton. */
|
|
7
|
+
private headers;
|
|
8
|
+
/**
|
|
9
|
+
* True for an @Endpoint(..., { formPost: true }) route: parse the body as
|
|
10
|
+
* application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not
|
|
11
|
+
* the request Content-Type header — the annotation is the single source of truth.
|
|
12
|
+
*/
|
|
13
|
+
private formPost;
|
|
14
|
+
constructor(clientMethod: (requestDto: unknown) => Promise<unknown>, path: string,
|
|
15
|
+
/** Owns the wire<->context transfer, both directions. Stateless framework singleton. */
|
|
16
|
+
headers: RequestContextHeaders,
|
|
17
|
+
/**
|
|
18
|
+
* True for an @Endpoint(..., { formPost: true }) route: parse the body as
|
|
19
|
+
* application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not
|
|
20
|
+
* the request Content-Type header — the annotation is the single source of truth.
|
|
21
|
+
*/
|
|
22
|
+
formPost?: boolean);
|
|
23
|
+
execute(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
24
|
+
executeTryCatch(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
25
|
+
executeImpl(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Read HTTP headers from Express request.
|
|
28
|
+
* Returns Map of header name (lowercase) -> array of values.
|
|
29
|
+
*
|
|
30
|
+
* HTTP spec allows multiple values for same header name.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* express Request -> webpieces {@link HttpRequest}. THE translation layer: below this line the
|
|
34
|
+
* filter chain and controllers never see express, which is what lets the same chain run
|
|
35
|
+
* in-process with no transport at all.
|
|
36
|
+
*/
|
|
37
|
+
private toWebpiecesRequest;
|
|
38
|
+
private readExpressHeaders;
|
|
39
|
+
/**
|
|
40
|
+
* Read raw request body as text.
|
|
41
|
+
* Used to manually parse JSON (instead of express.json() middleware).
|
|
42
|
+
*/
|
|
43
|
+
private readRequestBody;
|
|
44
|
+
/**
|
|
45
|
+
* Handle errors - translate to JSON ProtocolError (SYMMETRIC with ClientErrorTranslator).
|
|
46
|
+
* PUBLIC so wrapExpress can call it for symmetric error handling.
|
|
47
|
+
* Maps HttpError subclasses to appropriate HTTP status codes and ProtocolError response.
|
|
48
|
+
*
|
|
49
|
+
* Maps all HttpError types (must match ClientErrorTranslator.translateError()):
|
|
50
|
+
* - HttpUserError → 266 (with errorCode)
|
|
51
|
+
* - HttpBadRequestError → 400 (with field, guiAlertMessage)
|
|
52
|
+
* - HttpUnauthorizedError → 401
|
|
53
|
+
* - HttpForbiddenError → 403
|
|
54
|
+
* - HttpNotFoundError → 404
|
|
55
|
+
* - HttpTimeoutError → 408
|
|
56
|
+
* - HttpInternalServerError → 500
|
|
57
|
+
* - HttpBadGatewayError → 502
|
|
58
|
+
* - HttpGatewayTimeoutError → 504
|
|
59
|
+
* - HttpVendorError → 598 (with waitSeconds)
|
|
60
|
+
*/
|
|
61
|
+
handleError(res: Response, error: unknown): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExpressWrapper = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
6
|
+
// The logging backend prepends this logger name to every line, so messages below carry NO
|
|
7
|
+
// "[ExpressWrapper]" literal of their own — that would print the name twice.
|
|
8
|
+
const log = core_util_1.LogManager.getLogger('ExpressWrapper');
|
|
9
|
+
class ExpressWrapper {
|
|
10
|
+
clientMethod;
|
|
11
|
+
path;
|
|
12
|
+
headers;
|
|
13
|
+
formPost;
|
|
14
|
+
constructor(
|
|
15
|
+
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
16
|
+
clientMethod, path,
|
|
17
|
+
/** Owns the wire<->context transfer, both directions. Stateless framework singleton. */
|
|
18
|
+
headers,
|
|
19
|
+
/**
|
|
20
|
+
* True for an @Endpoint(..., { formPost: true }) route: parse the body as
|
|
21
|
+
* application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not
|
|
22
|
+
* the request Content-Type header — the annotation is the single source of truth.
|
|
23
|
+
*/
|
|
24
|
+
formPost = false) {
|
|
25
|
+
this.clientMethod = clientMethod;
|
|
26
|
+
this.path = path;
|
|
27
|
+
this.headers = headers;
|
|
28
|
+
this.formPost = formPost;
|
|
29
|
+
}
|
|
30
|
+
async execute(req, res, next) {
|
|
31
|
+
// MOVED: Wrap entire request in RequestContext.run()
|
|
32
|
+
// This establishes AsyncLocalStorage context for the request
|
|
33
|
+
await core_context_1.RequestContext.run(async () => {
|
|
34
|
+
await this.executeTryCatch(req, res, next);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async executeTryCatch(req, res, next) {
|
|
38
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- ExpressWrapper catches errors to translate to HTTP responses
|
|
39
|
+
try {
|
|
40
|
+
await this.executeImpl(req, res, next);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const error = (0, core_util_1.toError)(err);
|
|
44
|
+
// 5. Handle errors
|
|
45
|
+
this.handleError(res, error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async executeImpl(req, res, next) {
|
|
49
|
+
// 1. Translate express's request into the transport-neutral HttpRequest webpieces speaks.
|
|
50
|
+
const httpRequest = this.toWebpiecesRequest(req);
|
|
51
|
+
// 2. Parse the request body. The PARSER is chosen by the @Endpoint annotation (this.formPost),
|
|
52
|
+
// NOT the request Content-Type header — the annotation is the single source of truth.
|
|
53
|
+
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
54
|
+
let requestDto = {};
|
|
55
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
56
|
+
const bodyText = await this.readRequestBody(req);
|
|
57
|
+
if (this.formPost) {
|
|
58
|
+
// application/x-www-form-urlencoded → flat key→value. URLSearchParams is lenient
|
|
59
|
+
// (never throws) — right for EXTERNAL webhooks (e.g. Twilio) that post form-encoded.
|
|
60
|
+
requestDto = Object.fromEntries(new URLSearchParams(bodyText));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// JSON (default, SYMMETRIC with the client's JSON.stringify). A non-JSON body is a
|
|
64
|
+
// CLIENT error → 400, not the raw 500 an unguarded JSON.parse would throw.
|
|
65
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- translate parse failure to a 400 HttpError
|
|
66
|
+
try {
|
|
67
|
+
requestDto = bodyText ? JSON.parse(bodyText) : {};
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const error = (0, core_util_1.toError)(err);
|
|
71
|
+
throw new core_util_1.HttpBadRequestError('Request body is not valid JSON', undefined, undefined, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// 3. Publish the transport-neutral HttpRequest, then move its headers into the context and
|
|
76
|
+
// mint a request id if the caller sent none. BOTH happen above the api boundary, because
|
|
77
|
+
// http-routing requires an already-established, already-filled request scope — it never
|
|
78
|
+
// builds one for you. This is the "translation layer" every transport must provide.
|
|
79
|
+
this.headers.fillFromRequest(httpRequest);
|
|
80
|
+
// 4. Invoke the api CLIENT method — the SAME proxy tests use. Its filter chain + controller
|
|
81
|
+
// run here, reading the context filled above; the chain never touches express `req`.
|
|
82
|
+
const result = await this.clientMethod(requestDto);
|
|
83
|
+
// 5. Serialize the response DTO to JSON (SYMMETRIC with client's response.json())
|
|
84
|
+
const responseJson = JSON.stringify(result);
|
|
85
|
+
res.status(200).setHeader('Content-Type', 'application/json').send(responseJson);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Read HTTP headers from Express request.
|
|
89
|
+
* Returns Map of header name (lowercase) -> array of values.
|
|
90
|
+
*
|
|
91
|
+
* HTTP spec allows multiple values for same header name.
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* express Request -> webpieces {@link HttpRequest}. THE translation layer: below this line the
|
|
95
|
+
* filter chain and controllers never see express, which is what lets the same chain run
|
|
96
|
+
* in-process with no transport at all.
|
|
97
|
+
*/
|
|
98
|
+
toWebpiecesRequest(req) {
|
|
99
|
+
return new core_context_1.HttpRequest(req.method, this.path, this.readExpressHeaders(req));
|
|
100
|
+
}
|
|
101
|
+
readExpressHeaders(req) {
|
|
102
|
+
const headers = new Map();
|
|
103
|
+
// Express stores headers in req.headers as Record<string, string | string[]>
|
|
104
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
105
|
+
const lowerName = name.toLowerCase();
|
|
106
|
+
if (typeof value === 'string') {
|
|
107
|
+
headers.set(lowerName, [value]);
|
|
108
|
+
}
|
|
109
|
+
else if (Array.isArray(value)) {
|
|
110
|
+
headers.set(lowerName, value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return headers;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Read raw request body as text.
|
|
117
|
+
* Used to manually parse JSON (instead of express.json() middleware).
|
|
118
|
+
*/
|
|
119
|
+
async readRequestBody(req) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
let body = '';
|
|
122
|
+
req.on('data', (chunk) => {
|
|
123
|
+
body += chunk.toString();
|
|
124
|
+
});
|
|
125
|
+
req.on('end', () => {
|
|
126
|
+
resolve(body);
|
|
127
|
+
});
|
|
128
|
+
req.on('error', (err) => {
|
|
129
|
+
reject(err);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Handle errors - translate to JSON ProtocolError (SYMMETRIC with ClientErrorTranslator).
|
|
135
|
+
* PUBLIC so wrapExpress can call it for symmetric error handling.
|
|
136
|
+
* Maps HttpError subclasses to appropriate HTTP status codes and ProtocolError response.
|
|
137
|
+
*
|
|
138
|
+
* Maps all HttpError types (must match ClientErrorTranslator.translateError()):
|
|
139
|
+
* - HttpUserError → 266 (with errorCode)
|
|
140
|
+
* - HttpBadRequestError → 400 (with field, guiAlertMessage)
|
|
141
|
+
* - HttpUnauthorizedError → 401
|
|
142
|
+
* - HttpForbiddenError → 403
|
|
143
|
+
* - HttpNotFoundError → 404
|
|
144
|
+
* - HttpTimeoutError → 408
|
|
145
|
+
* - HttpInternalServerError → 500
|
|
146
|
+
* - HttpBadGatewayError → 502
|
|
147
|
+
* - HttpGatewayTimeoutError → 504
|
|
148
|
+
* - HttpVendorError → 598 (with waitSeconds)
|
|
149
|
+
*/
|
|
150
|
+
// webpieces-disable no-any-unknown -- a thrown error is genuinely unknown until narrowed below
|
|
151
|
+
handleError(res, error) {
|
|
152
|
+
if (res.headersSent) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// App-registered translations win, so an app can serialize its OWN error types (e.g. a
|
|
156
|
+
// custom 460) AND override built-ins. `undefined` means "not mine" — fall through to the
|
|
157
|
+
// built-in instanceof-HttpError ladder below, which stays the generic default. Symmetric
|
|
158
|
+
// with the client's ClientErrorTranslator, which consults tryTranslateFromWire() first.
|
|
159
|
+
if (error instanceof Error) {
|
|
160
|
+
const wire = core_util_1.ClientRegistry.tryTranslateToWire(error);
|
|
161
|
+
if (wire !== undefined) {
|
|
162
|
+
res.status(wire.statusCode)
|
|
163
|
+
.setHeader('Content-Type', 'application/json')
|
|
164
|
+
.send(JSON.stringify(wire.protocolError));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const protocolError = new core_util_1.ProtocolError();
|
|
169
|
+
if (error instanceof core_util_1.HttpError) {
|
|
170
|
+
// Set common fields for all HttpError types
|
|
171
|
+
protocolError.message = error.message;
|
|
172
|
+
protocolError.subType = error.subType;
|
|
173
|
+
protocolError.name = error.name;
|
|
174
|
+
// Set type-specific fields (MUST match ClientErrorTranslator)
|
|
175
|
+
if (error instanceof core_util_1.HttpUserError) {
|
|
176
|
+
log.info(`User Error: ${error.message}`);
|
|
177
|
+
protocolError.errorCode = error.errorCode;
|
|
178
|
+
}
|
|
179
|
+
else if (error instanceof core_util_1.HttpBadRequestError) {
|
|
180
|
+
log.info(`Bad Request: ${error.message}`);
|
|
181
|
+
protocolError.field = error.field;
|
|
182
|
+
protocolError.guiAlertMessage = error.guiMessage;
|
|
183
|
+
}
|
|
184
|
+
else if (error instanceof core_util_1.HttpNotFoundError) {
|
|
185
|
+
log.info(`Not Found: ${error.message}`);
|
|
186
|
+
}
|
|
187
|
+
else if (error instanceof core_util_1.HttpTimeoutError) {
|
|
188
|
+
log.error(`Timeout Error: ${error.message}`);
|
|
189
|
+
}
|
|
190
|
+
else if (error instanceof core_util_1.HttpVendorError) {
|
|
191
|
+
log.error(`Vendor Error: ${error.message}`);
|
|
192
|
+
protocolError.waitSeconds = error.waitSeconds;
|
|
193
|
+
}
|
|
194
|
+
else if (error instanceof core_util_1.HttpUnauthorizedError) {
|
|
195
|
+
log.info(`Unauthorized: ${error.message}`);
|
|
196
|
+
}
|
|
197
|
+
else if (error instanceof core_util_1.HttpForbiddenError) {
|
|
198
|
+
log.info(`Forbidden: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
else if (error instanceof core_util_1.HttpInternalServerError) {
|
|
201
|
+
log.error(`Internal Server Error: ${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
else if (error instanceof core_util_1.HttpBadGatewayError) {
|
|
204
|
+
log.error(`Bad Gateway: ${error.message}`);
|
|
205
|
+
}
|
|
206
|
+
else if (error instanceof core_util_1.HttpGatewayTimeoutError) {
|
|
207
|
+
log.error(`Gateway Timeout: ${error.message}`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
log.info(`Generic HttpError: ${error.message}`);
|
|
211
|
+
}
|
|
212
|
+
// Serialize ProtocolError to JSON (SYMMETRIC with client)
|
|
213
|
+
const responseJson = JSON.stringify(protocolError);
|
|
214
|
+
res.status(error.code).setHeader('Content-Type', 'application/json').send(responseJson);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Unknown error - 500
|
|
218
|
+
const err = (0, core_util_1.toError)(error);
|
|
219
|
+
protocolError.message = 'Internal Server Error';
|
|
220
|
+
log.error('Unexpected error:', err);
|
|
221
|
+
const responseJson = JSON.stringify(protocolError);
|
|
222
|
+
res.status(500).setHeader('Content-Type', 'application/json').send(responseJson);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
exports.ExpressWrapper = ExpressWrapper;
|
|
227
|
+
//# sourceMappingURL=ExpressWrapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpressWrapper.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/ExpressWrapper.ts"],"names":[],"mappings":"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","sourcesContent":["import { Request, Response, NextFunction } from 'express';\nimport {\n ProtocolError,\n ClientRegistry,\n HttpError,\n HttpBadRequestError,\n HttpVendorError,\n HttpUserError,\n HttpNotFoundError,\n HttpTimeoutError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpInternalServerError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n toError,\n LogManager,\n} from '@webpieces/core-util';\nimport { RequestContext, HttpRequest, RequestContextHeaders } from '@webpieces/core-context';\n\n// The logging backend prepends this logger name to every line, so messages below carry NO\n// \"[ExpressWrapper]\" literal of their own — that would print the name twice.\nconst log = LogManager.getLogger('ExpressWrapper');\n\nexport class ExpressWrapper {\n constructor(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private clientMethod: (requestDto: unknown) => Promise<unknown>,\n private path: string,\n /** Owns the wire<->context transfer, both directions. Stateless framework singleton. */\n private headers: RequestContextHeaders,\n /**\n * True for an @Endpoint(..., { formPost: true }) route: parse the body as\n * application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not\n * the request Content-Type header — the annotation is the single source of truth.\n */\n private formPost: boolean = false,\n ) {\n }\n\n public async execute(req: Request, res: Response, next: NextFunction): Promise<void> {\n // MOVED: Wrap entire request in RequestContext.run()\n // This establishes AsyncLocalStorage context for the request\n await RequestContext.run(async () => {\n await this.executeTryCatch(req, res, next);\n });\n }\n\n public async executeTryCatch(req: Request, res: Response, next: NextFunction): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- ExpressWrapper catches errors to translate to HTTP responses\n try {\n await this.executeImpl(req, res, next);\n } catch (err: unknown) {\n const error = toError(err);\n // 5. Handle errors\n this.handleError(res, error);\n }\n }\n\n public async executeImpl(req: Request, res: Response, next: NextFunction): Promise<void> {\n // 1. Translate express's request into the transport-neutral HttpRequest webpieces speaks.\n const httpRequest = this.toWebpiecesRequest(req);\n\n // 2. Parse the request body. The PARSER is chosen by the @Endpoint annotation (this.formPost),\n // NOT the request Content-Type header — the annotation is the single source of truth.\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n let requestDto: unknown = {};\n if (['POST', 'PUT', 'PATCH'].includes(req.method)) {\n const bodyText = await this.readRequestBody(req);\n if (this.formPost) {\n // application/x-www-form-urlencoded → flat key→value. URLSearchParams is lenient\n // (never throws) — right for EXTERNAL webhooks (e.g. Twilio) that post form-encoded.\n requestDto = Object.fromEntries(new URLSearchParams(bodyText));\n } else {\n // JSON (default, SYMMETRIC with the client's JSON.stringify). A non-JSON body is a\n // CLIENT error → 400, not the raw 500 an unguarded JSON.parse would throw.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- translate parse failure to a 400 HttpError\n try {\n requestDto = bodyText ? JSON.parse(bodyText) : {};\n } catch (err: unknown) {\n const error = toError(err);\n throw new HttpBadRequestError('Request body is not valid JSON', undefined, undefined, error);\n }\n }\n }\n\n // 3. Publish the transport-neutral HttpRequest, then move its headers into the context and\n // mint a request id if the caller sent none. BOTH happen above the api boundary, because\n // http-routing requires an already-established, already-filled request scope — it never\n // builds one for you. This is the \"translation layer\" every transport must provide.\n this.headers.fillFromRequest(httpRequest);\n\n // 4. Invoke the api CLIENT method — the SAME proxy tests use. Its filter chain + controller\n // run here, reading the context filled above; the chain never touches express `req`.\n const result = await this.clientMethod(requestDto);\n\n // 5. Serialize the response DTO to JSON (SYMMETRIC with client's response.json())\n const responseJson = JSON.stringify(result);\n res.status(200).setHeader('Content-Type', 'application/json').send(responseJson);\n }\n\n /**\n * Read HTTP headers from Express request.\n * Returns Map of header name (lowercase) -> array of values.\n *\n * HTTP spec allows multiple values for same header name.\n */\n /**\n * express Request -> webpieces {@link HttpRequest}. THE translation layer: below this line the\n * filter chain and controllers never see express, which is what lets the same chain run\n * in-process with no transport at all.\n */\n private toWebpiecesRequest(req: Request): HttpRequest {\n return new HttpRequest(req.method, this.path, this.readExpressHeaders(req));\n }\n\n private readExpressHeaders(req: Request): Map<string, string[]> {\n const headers = new Map<string, string[]>();\n\n // Express stores headers in req.headers as Record<string, string | string[]>\n for (const [name, value] of Object.entries(req.headers)) {\n const lowerName = name.toLowerCase();\n\n if (typeof value === 'string') {\n headers.set(lowerName, [value]);\n } else if (Array.isArray(value)) {\n headers.set(lowerName, value);\n }\n }\n\n return headers;\n }\n\n /**\n * Read raw request body as text.\n * Used to manually parse JSON (instead of express.json() middleware).\n */\n private async readRequestBody(req: Request): Promise<string> {\n return new Promise((resolve: (body: string) => void, reject: (err: Error) => void) => {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString();\n });\n req.on('end', () => {\n resolve(body);\n });\n req.on('error', (err: Error) => {\n reject(err);\n });\n });\n }\n\n /**\n * Handle errors - translate to JSON ProtocolError (SYMMETRIC with ClientErrorTranslator).\n * PUBLIC so wrapExpress can call it for symmetric error handling.\n * Maps HttpError subclasses to appropriate HTTP status codes and ProtocolError response.\n *\n * Maps all HttpError types (must match ClientErrorTranslator.translateError()):\n * - HttpUserError → 266 (with errorCode)\n * - HttpBadRequestError → 400 (with field, guiAlertMessage)\n * - HttpUnauthorizedError → 401\n * - HttpForbiddenError → 403\n * - HttpNotFoundError → 404\n * - HttpTimeoutError → 408\n * - HttpInternalServerError → 500\n * - HttpBadGatewayError → 502\n * - HttpGatewayTimeoutError → 504\n * - HttpVendorError → 598 (with waitSeconds)\n */\n // webpieces-disable no-any-unknown -- a thrown error is genuinely unknown until narrowed below\n public handleError(res: Response, error: unknown): void {\n if (res.headersSent) {\n return;\n }\n\n // App-registered translations win, so an app can serialize its OWN error types (e.g. a\n // custom 460) AND override built-ins. `undefined` means \"not mine\" — fall through to the\n // built-in instanceof-HttpError ladder below, which stays the generic default. Symmetric\n // with the client's ClientErrorTranslator, which consults tryTranslateFromWire() first.\n if (error instanceof Error) {\n const wire = ClientRegistry.tryTranslateToWire(error);\n if (wire !== undefined) {\n res.status(wire.statusCode)\n .setHeader('Content-Type', 'application/json')\n .send(JSON.stringify(wire.protocolError));\n return;\n }\n }\n\n const protocolError = new ProtocolError();\n\n if (error instanceof HttpError) {\n // Set common fields for all HttpError types\n protocolError.message = error.message;\n protocolError.subType = error.subType;\n protocolError.name = error.name;\n\n // Set type-specific fields (MUST match ClientErrorTranslator)\n if (error instanceof HttpUserError) {\n log.info(`User Error: ${error.message}`);\n protocolError.errorCode = error.errorCode;\n } else if (error instanceof HttpBadRequestError) {\n log.info(`Bad Request: ${error.message}`);\n protocolError.field = error.field;\n protocolError.guiAlertMessage = error.guiMessage;\n } else if (error instanceof HttpNotFoundError) {\n log.info(`Not Found: ${error.message}`);\n } else if (error instanceof HttpTimeoutError) {\n log.error(`Timeout Error: ${error.message}`);\n } else if (error instanceof HttpVendorError) {\n log.error(`Vendor Error: ${error.message}`);\n protocolError.waitSeconds = error.waitSeconds;\n } else if (error instanceof HttpUnauthorizedError) {\n log.info(`Unauthorized: ${error.message}`);\n } else if (error instanceof HttpForbiddenError) {\n log.info(`Forbidden: ${error.message}`);\n } else if (error instanceof HttpInternalServerError) {\n log.error(`Internal Server Error: ${error.message}`);\n } else if (error instanceof HttpBadGatewayError) {\n log.error(`Bad Gateway: ${error.message}`);\n } else if (error instanceof HttpGatewayTimeoutError) {\n log.error(`Gateway Timeout: ${error.message}`);\n } else {\n log.info(`Generic HttpError: ${error.message}`);\n }\n\n // Serialize ProtocolError to JSON (SYMMETRIC with client)\n const responseJson = JSON.stringify(protocolError);\n res.status(error.code).setHeader('Content-Type', 'application/json').send(responseJson);\n } else {\n // Unknown error - 500\n const err = toError(error);\n protocolError.message = 'Internal Server Error';\n log.error('Unexpected error:', err);\n const responseJson = JSON.stringify(protocolError);\n res.status(500).setHeader('Content-Type', 'application/json').send(responseJson);\n }\n }\n}\n"]}
|
|
@@ -44,7 +44,7 @@ class WebpiecesExpressRouter {
|
|
|
44
44
|
for (const apiClient of this.apiFactory.apiClients()) {
|
|
45
45
|
count += this.mountApiClient(app, apiClient);
|
|
46
46
|
}
|
|
47
|
-
log.info(`
|
|
47
|
+
log.info(`Mounted ${count} webpieces route(s) onto express`);
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Add the webpieces global middleware (HTML error page, optional CORS, request logging), bind
|
|
@@ -71,11 +71,11 @@ class WebpiecesExpressRouter {
|
|
|
71
71
|
return new Promise((resolve, reject) => {
|
|
72
72
|
const server = app.listen(port, (error) => {
|
|
73
73
|
if (error) {
|
|
74
|
-
log.error(`
|
|
74
|
+
log.error(`Failed to start on port ${port}:`, error);
|
|
75
75
|
reject(error);
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
log.info(`
|
|
78
|
+
log.info(`Listening on http://localhost:${port}`);
|
|
79
79
|
resolve(server);
|
|
80
80
|
});
|
|
81
81
|
});
|
|
@@ -120,7 +120,7 @@ class WebpiecesExpressRouter {
|
|
|
120
120
|
app.patch(path, expressHandler);
|
|
121
121
|
break;
|
|
122
122
|
default:
|
|
123
|
-
log.warn(`
|
|
123
|
+
log.warn(`Unknown HTTP method: ${httpMethod}`);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesExpressRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesExpressRouter.ts"],"names":[],"mappings":";;;AACA,0DAAuH;AACvH,oDAAkD;AAClD,+DAAiF;AAEjF,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;AAK3D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAa,sBAAsB;IAGF;IAFZ,UAAU,GAAG,IAAI,yCAAmB,EAAE,CAAC;IAExD,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAEvD;;;;;;OAMG;IACH,WAAW,CAAC,GAAY;QACpB,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,EAAE,CAAC;YACnD,KAAK,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QACjD,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"WebpiecesExpressRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesExpressRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Express } from 'express';\nimport { ApiFactory, ApiClient, getApiPath, getEndpoints, isFormPost, WebpiecesConfig } from '@webpieces/http-routing';\nimport { LogManager } from '@webpieces/core-util';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\n\nconst log = LogManager.getLogger('WebpiecesExpressRouter');\n\n/** The value returned by express `app.listen(...)` (a node http.Server). */\ntype HttpServer = ReturnType<Express['listen']>;\n\n/**\n * WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only\n * {@link ApiFactory} (a WebpiecesRouter). It is the ONLY place express lifecycle lives.\n *\n * It never reaches into routing internals: it asks the ApiFactory for `apiClients()` — each\n * an api + routeMeta + composed filter-chain→controller impl — and binds each to an express\n * route (`app.<verb>(path, handler)`) invoked when the matching HTTP request arrives. The\n * RouteBuilder stays hidden inside the ApiFactory.\n *\n * ```typescript\n * const apiFactory = await WebpiecesRouterFactory.create(config, { appBindings });\n * apiFactory.addRoutes(SaveApi, SaveController);\n * const express = new WebpiecesExpressRouter(apiFactory);\n *\n * // legacy / side-by-side: mount onto an existing app; you own listen + your middleware\n * express.bindExpress(existingApp);\n *\n * // non-legacy: add webpieces global middleware + listen for you\n * await express.bindAndStartExpress(express(), 8080);\n * ```\n */\nexport class WebpiecesExpressRouter {\n private readonly middleware = new WebpiecesMiddleware();\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n /**\n * Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,\n * express-tier + api-tier filter chain, error→JSON) onto the caller's express app.\n *\n * Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose other\n * routes must stay untouched. The caller owns app.listen() and any global middleware.\n */\n bindExpress(app: Express): void {\n let count = 0;\n for (const apiClient of this.apiFactory.apiClients()) {\n count += this.mountApiClient(app, apiClient);\n }\n log.info(`Mounted ${count} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (HTML error page, optional CORS, request logging), bind\n * the routes, then app.listen(port). Convenience for a non-legacy webpieces server where\n * webpieces owns the whole express app. Resolves with the http.Server once listening.\n *\n * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and\n * {@link WebpiecesMiddleware.corsMiddleware}.\n */\n async bindAndStartExpress(\n app: Express,\n port: number = 8080,\n config?: WebpiecesConfig,\n ): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n app.use(this.middleware.globalErrorHandler.bind(this.middleware));\n\n // CORS is OPT-IN, and stays OFF in production. A server that serves its own browser app does\n // not need it — a browser applies no cors check to a same-origin request — so mounting it\n // would only hand credentialed cross-origin read access to whatever it allows, for nothing.\n // It is needed solely when a browser on ANOTHER origin calls this api: `ng serve` in dev, or\n // a UI hosted on a different host. Those say so via corsOrigins.\n const corsOrigins = config?.corsOrigins ?? [];\n if (corsOrigins.length > 0) {\n app.use(this.middleware.corsMiddleware(config));\n }\n\n app.use(this.middleware.logNextLayer.bind(this.middleware));\n\n this.bindExpress(app);\n\n return new Promise<HttpServer>(\n (resolve: (server: HttpServer) => void, reject: (err: Error) => void) => {\n const server: HttpServer = app.listen(port, (error?: Error) => {\n if (error) {\n log.error(`Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`Listening on http://localhost:${port}`);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;\n * for each we wrap the matching client method (the proxy — RequestContext.run + header read +\n * JSON body parse + error→ProtocolError all live in the wrapper/chain) and register the route.\n * This is one-to-one with a test: an HTTP POST maps straight to `client[method](dto)`.\n *\n * @returns the number of routes mounted for this api.\n */\n private mountApiClient(app: Express, apiClient: ApiClient): number {\n const basePath = getApiPath(apiClient.api) || '';\n const endpoints = getEndpoints(apiClient.api) || {};\n let count = 0;\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const path = basePath + endpointPath;\n // The parser is chosen by the @Endpoint annotation, not the request Content-Type.\n const wrapper = this.middleware.createExpressWrapper(\n apiClient.client[methodName],\n path,\n isFormPost(apiClient.api, methodName),\n );\n // All webpieces routes are POST (the api-tier convention).\n this.registerHandler(app, 'POST', path, wrapper.execute.bind(wrapper));\n count++;\n }\n return count;\n }\n\n private registerHandler(\n app: Express,\n httpMethod: string,\n path: string,\n expressHandler: ExpressRouteHandler,\n ): void {\n switch (httpMethod.toLowerCase()) {\n case 'get':\n app.get(path, expressHandler);\n break;\n case 'post':\n app.post(path, expressHandler);\n break;\n case 'put':\n app.put(path, expressHandler);\n break;\n case 'delete':\n app.delete(path, expressHandler);\n break;\n case 'patch':\n app.patch(path, expressHandler);\n break;\n default:\n log.warn(`Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
|
|
@@ -1,72 +1,12 @@
|
|
|
1
1
|
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
2
|
import { WebpiecesConfig } from '@webpieces/http-routing';
|
|
3
|
-
import {
|
|
3
|
+
import { ExpressWrapper } from './ExpressWrapper';
|
|
4
4
|
/**
|
|
5
5
|
* Express route handler function type. Lives in http-server (the express adapter),
|
|
6
6
|
* NOT in the node-only http-routing package, so http-routing stays express-free.
|
|
7
7
|
* Used by WebpiecesExpressRouter to register handlers Express can call.
|
|
8
8
|
*/
|
|
9
9
|
export type ExpressRouteHandler = (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
10
|
-
export declare class ExpressWrapper {
|
|
11
|
-
private clientMethod;
|
|
12
|
-
private path;
|
|
13
|
-
/** Owns the wire<->context transfer, both directions. Stateless framework singleton. */
|
|
14
|
-
private headers;
|
|
15
|
-
/**
|
|
16
|
-
* True for an @Endpoint(..., { formPost: true }) route: parse the body as
|
|
17
|
-
* application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not
|
|
18
|
-
* the request Content-Type header — the annotation is the single source of truth.
|
|
19
|
-
*/
|
|
20
|
-
private formPost;
|
|
21
|
-
constructor(clientMethod: (requestDto: unknown) => Promise<unknown>, path: string,
|
|
22
|
-
/** Owns the wire<->context transfer, both directions. Stateless framework singleton. */
|
|
23
|
-
headers: RequestContextHeaders,
|
|
24
|
-
/**
|
|
25
|
-
* True for an @Endpoint(..., { formPost: true }) route: parse the body as
|
|
26
|
-
* application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not
|
|
27
|
-
* the request Content-Type header — the annotation is the single source of truth.
|
|
28
|
-
*/
|
|
29
|
-
formPost?: boolean);
|
|
30
|
-
execute(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
31
|
-
executeTryCatch(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
32
|
-
executeImpl(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
* Read HTTP headers from Express request.
|
|
35
|
-
* Returns Map of header name (lowercase) -> array of values.
|
|
36
|
-
*
|
|
37
|
-
* HTTP spec allows multiple values for same header name.
|
|
38
|
-
*/
|
|
39
|
-
/**
|
|
40
|
-
* express Request -> webpieces {@link HttpRequest}. THE translation layer: below this line the
|
|
41
|
-
* filter chain and controllers never see express, which is what lets the same chain run
|
|
42
|
-
* in-process with no transport at all.
|
|
43
|
-
*/
|
|
44
|
-
private toWebpiecesRequest;
|
|
45
|
-
private readExpressHeaders;
|
|
46
|
-
/**
|
|
47
|
-
* Read raw request body as text.
|
|
48
|
-
* Used to manually parse JSON (instead of express.json() middleware).
|
|
49
|
-
*/
|
|
50
|
-
private readRequestBody;
|
|
51
|
-
/**
|
|
52
|
-
* Handle errors - translate to JSON ProtocolError (SYMMETRIC with ClientErrorTranslator).
|
|
53
|
-
* PUBLIC so wrapExpress can call it for symmetric error handling.
|
|
54
|
-
* Maps HttpError subclasses to appropriate HTTP status codes and ProtocolError response.
|
|
55
|
-
*
|
|
56
|
-
* Maps all HttpError types (must match ClientErrorTranslator.translateError()):
|
|
57
|
-
* - HttpUserError → 266 (with errorCode)
|
|
58
|
-
* - HttpBadRequestError → 400 (with field, guiAlertMessage)
|
|
59
|
-
* - HttpUnauthorizedError → 401
|
|
60
|
-
* - HttpForbiddenError → 403
|
|
61
|
-
* - HttpNotFoundError → 404
|
|
62
|
-
* - HttpTimeoutError → 408
|
|
63
|
-
* - HttpInternalServerError → 500
|
|
64
|
-
* - HttpBadGatewayError → 502
|
|
65
|
-
* - HttpGatewayTimeoutError → 504
|
|
66
|
-
* - HttpVendorError → 598 (with waitSeconds)
|
|
67
|
-
*/
|
|
68
|
-
handleError(res: Response, error: unknown): void;
|
|
69
|
-
}
|
|
70
10
|
/**
|
|
71
11
|
* WebpiecesMiddleware - Express middleware for WebPieces server.
|
|
72
12
|
*
|
|
@@ -1,230 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WebpiecesMiddleware =
|
|
3
|
+
exports.WebpiecesMiddleware = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const cors_1 = tslib_1.__importDefault(require("cors"));
|
|
6
6
|
const http_routing_1 = require("@webpieces/http-routing");
|
|
7
7
|
const core_util_1 = require("@webpieces/core-util");
|
|
8
|
-
const core_util_2 = require("@webpieces/core-util");
|
|
9
8
|
const core_context_1 = require("@webpieces/core-context");
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
formPost;
|
|
17
|
-
constructor(
|
|
18
|
-
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
19
|
-
clientMethod, path,
|
|
20
|
-
/** Owns the wire<->context transfer, both directions. Stateless framework singleton. */
|
|
21
|
-
headers,
|
|
22
|
-
/**
|
|
23
|
-
* True for an @Endpoint(..., { formPost: true }) route: parse the body as
|
|
24
|
-
* application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not
|
|
25
|
-
* the request Content-Type header — the annotation is the single source of truth.
|
|
26
|
-
*/
|
|
27
|
-
formPost = false) {
|
|
28
|
-
this.clientMethod = clientMethod;
|
|
29
|
-
this.path = path;
|
|
30
|
-
this.headers = headers;
|
|
31
|
-
this.formPost = formPost;
|
|
32
|
-
}
|
|
33
|
-
async execute(req, res, next) {
|
|
34
|
-
// MOVED: Wrap entire request in RequestContext.run()
|
|
35
|
-
// This establishes AsyncLocalStorage context for the request
|
|
36
|
-
await core_context_1.RequestContext.run(async () => {
|
|
37
|
-
await this.executeTryCatch(req, res, next);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
async executeTryCatch(req, res, next) {
|
|
41
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- ExpressWrapper catches errors to translate to HTTP responses
|
|
42
|
-
try {
|
|
43
|
-
await this.executeImpl(req, res, next);
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
const error = (0, core_util_2.toError)(err);
|
|
47
|
-
// 5. Handle errors
|
|
48
|
-
this.handleError(res, error);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
async executeImpl(req, res, next) {
|
|
52
|
-
// 1. Translate express's request into the transport-neutral HttpRequest webpieces speaks.
|
|
53
|
-
const httpRequest = this.toWebpiecesRequest(req);
|
|
54
|
-
// 2. Parse the request body. The PARSER is chosen by the @Endpoint annotation (this.formPost),
|
|
55
|
-
// NOT the request Content-Type header — the annotation is the single source of truth.
|
|
56
|
-
let requestDto = {};
|
|
57
|
-
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
58
|
-
const bodyText = await this.readRequestBody(req);
|
|
59
|
-
if (this.formPost) {
|
|
60
|
-
// application/x-www-form-urlencoded → flat key→value. URLSearchParams is lenient
|
|
61
|
-
// (never throws) — right for EXTERNAL webhooks (e.g. Twilio) that post form-encoded.
|
|
62
|
-
requestDto = Object.fromEntries(new URLSearchParams(bodyText));
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
// JSON (default, SYMMETRIC with the client's JSON.stringify). A non-JSON body is a
|
|
66
|
-
// CLIENT error → 400, not the raw 500 an unguarded JSON.parse would throw.
|
|
67
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- translate parse failure to a 400 HttpError
|
|
68
|
-
try {
|
|
69
|
-
requestDto = bodyText ? JSON.parse(bodyText) : {};
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
const error = (0, core_util_2.toError)(err);
|
|
73
|
-
throw new core_util_1.HttpBadRequestError('Request body is not valid JSON', undefined, undefined, error);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// 3. Publish the transport-neutral HttpRequest, then move its headers into the context and
|
|
78
|
-
// mint a request id if the caller sent none. BOTH happen above the api boundary, because
|
|
79
|
-
// http-routing requires an already-established, already-filled request scope — it never
|
|
80
|
-
// builds one for you. This is the "translation layer" every transport must provide.
|
|
81
|
-
this.headers.fillFromRequest(httpRequest);
|
|
82
|
-
// 4. Invoke the api CLIENT method — the SAME proxy tests use. Its filter chain + controller
|
|
83
|
-
// run here, reading the context filled above; the chain never touches express `req`.
|
|
84
|
-
const result = await this.clientMethod(requestDto);
|
|
85
|
-
// 5. Serialize the response DTO to JSON (SYMMETRIC with client's response.json())
|
|
86
|
-
const responseJson = JSON.stringify(result);
|
|
87
|
-
res.status(200).setHeader('Content-Type', 'application/json').send(responseJson);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Read HTTP headers from Express request.
|
|
91
|
-
* Returns Map of header name (lowercase) -> array of values.
|
|
92
|
-
*
|
|
93
|
-
* HTTP spec allows multiple values for same header name.
|
|
94
|
-
*/
|
|
95
|
-
/**
|
|
96
|
-
* express Request -> webpieces {@link HttpRequest}. THE translation layer: below this line the
|
|
97
|
-
* filter chain and controllers never see express, which is what lets the same chain run
|
|
98
|
-
* in-process with no transport at all.
|
|
99
|
-
*/
|
|
100
|
-
toWebpiecesRequest(req) {
|
|
101
|
-
return new core_context_1.HttpRequest(req.method, this.path, this.readExpressHeaders(req));
|
|
102
|
-
}
|
|
103
|
-
readExpressHeaders(req) {
|
|
104
|
-
const headers = new Map();
|
|
105
|
-
// Express stores headers in req.headers as Record<string, string | string[]>
|
|
106
|
-
for (const [name, value] of Object.entries(req.headers)) {
|
|
107
|
-
const lowerName = name.toLowerCase();
|
|
108
|
-
if (typeof value === 'string') {
|
|
109
|
-
headers.set(lowerName, [value]);
|
|
110
|
-
}
|
|
111
|
-
else if (Array.isArray(value)) {
|
|
112
|
-
headers.set(lowerName, value);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return headers;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Read raw request body as text.
|
|
119
|
-
* Used to manually parse JSON (instead of express.json() middleware).
|
|
120
|
-
*/
|
|
121
|
-
async readRequestBody(req) {
|
|
122
|
-
return new Promise((resolve, reject) => {
|
|
123
|
-
let body = '';
|
|
124
|
-
req.on('data', (chunk) => {
|
|
125
|
-
body += chunk.toString();
|
|
126
|
-
});
|
|
127
|
-
req.on('end', () => {
|
|
128
|
-
resolve(body);
|
|
129
|
-
});
|
|
130
|
-
req.on('error', (err) => {
|
|
131
|
-
reject(err);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Handle errors - translate to JSON ProtocolError (SYMMETRIC with ClientErrorTranslator).
|
|
137
|
-
* PUBLIC so wrapExpress can call it for symmetric error handling.
|
|
138
|
-
* Maps HttpError subclasses to appropriate HTTP status codes and ProtocolError response.
|
|
139
|
-
*
|
|
140
|
-
* Maps all HttpError types (must match ClientErrorTranslator.translateError()):
|
|
141
|
-
* - HttpUserError → 266 (with errorCode)
|
|
142
|
-
* - HttpBadRequestError → 400 (with field, guiAlertMessage)
|
|
143
|
-
* - HttpUnauthorizedError → 401
|
|
144
|
-
* - HttpForbiddenError → 403
|
|
145
|
-
* - HttpNotFoundError → 404
|
|
146
|
-
* - HttpTimeoutError → 408
|
|
147
|
-
* - HttpInternalServerError → 500
|
|
148
|
-
* - HttpBadGatewayError → 502
|
|
149
|
-
* - HttpGatewayTimeoutError → 504
|
|
150
|
-
* - HttpVendorError → 598 (with waitSeconds)
|
|
151
|
-
*/
|
|
152
|
-
handleError(res, error) {
|
|
153
|
-
if (res.headersSent) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
// App-registered translations win, so an app can serialize its OWN error types (e.g. a
|
|
157
|
-
// custom 460) AND override built-ins. `undefined` means "not mine" — fall through to the
|
|
158
|
-
// built-in instanceof-HttpError ladder below, which stays the generic default. Symmetric
|
|
159
|
-
// with the client's ClientErrorTranslator, which consults tryTranslateFromWire() first.
|
|
160
|
-
if (error instanceof Error) {
|
|
161
|
-
const wire = core_util_1.ClientRegistry.tryTranslateToWire(error);
|
|
162
|
-
if (wire !== undefined) {
|
|
163
|
-
res.status(wire.statusCode)
|
|
164
|
-
.setHeader('Content-Type', 'application/json')
|
|
165
|
-
.send(JSON.stringify(wire.protocolError));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const protocolError = new core_util_1.ProtocolError();
|
|
170
|
-
if (error instanceof core_util_1.HttpError) {
|
|
171
|
-
// Set common fields for all HttpError types
|
|
172
|
-
protocolError.message = error.message;
|
|
173
|
-
protocolError.subType = error.subType;
|
|
174
|
-
protocolError.name = error.name;
|
|
175
|
-
// Set type-specific fields (MUST match ClientErrorTranslator)
|
|
176
|
-
if (error instanceof core_util_1.HttpUserError) {
|
|
177
|
-
log.info(`[ExpressWrapper] User Error: ${error.message}`);
|
|
178
|
-
protocolError.errorCode = error.errorCode;
|
|
179
|
-
}
|
|
180
|
-
else if (error instanceof core_util_1.HttpBadRequestError) {
|
|
181
|
-
log.info(`[ExpressWrapper] Bad Request: ${error.message}`);
|
|
182
|
-
protocolError.field = error.field;
|
|
183
|
-
protocolError.guiAlertMessage = error.guiMessage;
|
|
184
|
-
}
|
|
185
|
-
else if (error instanceof core_util_1.HttpNotFoundError) {
|
|
186
|
-
log.info(`[ExpressWrapper] Not Found: ${error.message}`);
|
|
187
|
-
}
|
|
188
|
-
else if (error instanceof core_util_1.HttpTimeoutError) {
|
|
189
|
-
log.error(`[ExpressWrapper] Timeout Error: ${error.message}`);
|
|
190
|
-
}
|
|
191
|
-
else if (error instanceof core_util_1.HttpVendorError) {
|
|
192
|
-
log.error(`[ExpressWrapper] Vendor Error: ${error.message}`);
|
|
193
|
-
protocolError.waitSeconds = error.waitSeconds;
|
|
194
|
-
}
|
|
195
|
-
else if (error instanceof core_util_1.HttpUnauthorizedError) {
|
|
196
|
-
log.info(`[ExpressWrapper] Unauthorized: ${error.message}`);
|
|
197
|
-
}
|
|
198
|
-
else if (error instanceof core_util_1.HttpForbiddenError) {
|
|
199
|
-
log.info(`[ExpressWrapper] Forbidden: ${error.message}`);
|
|
200
|
-
}
|
|
201
|
-
else if (error instanceof core_util_1.HttpInternalServerError) {
|
|
202
|
-
log.error(`[ExpressWrapper] Internal Server Error: ${error.message}`);
|
|
203
|
-
}
|
|
204
|
-
else if (error instanceof core_util_1.HttpBadGatewayError) {
|
|
205
|
-
log.error(`[ExpressWrapper] Bad Gateway: ${error.message}`);
|
|
206
|
-
}
|
|
207
|
-
else if (error instanceof core_util_1.HttpGatewayTimeoutError) {
|
|
208
|
-
log.error(`[ExpressWrapper] Gateway Timeout: ${error.message}`);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
log.info(`[ExpressWrapper] Generic HttpError: ${error.message}`);
|
|
212
|
-
}
|
|
213
|
-
// Serialize ProtocolError to JSON (SYMMETRIC with client)
|
|
214
|
-
const responseJson = JSON.stringify(protocolError);
|
|
215
|
-
res.status(error.code).setHeader('Content-Type', 'application/json').send(responseJson);
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
// Unknown error - 500
|
|
219
|
-
const err = (0, core_util_2.toError)(error);
|
|
220
|
-
protocolError.message = 'Internal Server Error';
|
|
221
|
-
log.error('[ExpressWrapper] Unexpected error:', err);
|
|
222
|
-
const responseJson = JSON.stringify(protocolError);
|
|
223
|
-
res.status(500).setHeader('Content-Type', 'application/json').send(responseJson);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
exports.ExpressWrapper = ExpressWrapper;
|
|
9
|
+
const core_util_2 = require("@webpieces/core-util");
|
|
10
|
+
const ExpressWrapper_1 = require("./ExpressWrapper");
|
|
11
|
+
const log = core_util_2.LogManager.getLogger('WebpiecesMiddleware');
|
|
12
|
+
// CORS mount/allow/block lines log under their own name (not [WebpiecesMiddleware]); the backend
|
|
13
|
+
// prepends "[CORS]" for us, so the message strings below carry no literal prefix of their own.
|
|
14
|
+
const corsLog = core_util_2.LogManager.getLogger('CORS');
|
|
228
15
|
/**
|
|
229
16
|
* WebpiecesMiddleware - Express middleware for WebPieces server.
|
|
230
17
|
*
|
|
@@ -270,7 +57,7 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
270
57
|
log.info(`🔴 [Layer 1: GlobalErrorHandler] Request END (success): ${req.method} ${req.path}`);
|
|
271
58
|
}
|
|
272
59
|
catch (err) {
|
|
273
|
-
const error = (0,
|
|
60
|
+
const error = (0, core_util_1.toError)(err);
|
|
274
61
|
log.error('🔴 [Layer 1: GlobalErrorHandler] Caught unhandled error:', error);
|
|
275
62
|
if (!res.headersSent) {
|
|
276
63
|
// Return HTML error page (not JSON - JsonTranslator handles JSON errors)
|
|
@@ -329,7 +116,7 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
329
116
|
*/
|
|
330
117
|
corsMiddleware(config) {
|
|
331
118
|
const allowedOrigins = config?.corsOrigins ?? [];
|
|
332
|
-
|
|
119
|
+
corsLog.info(`CORS MOUNTED. Allowing same-origin + [${allowedOrigins.join(', ')}]. ` +
|
|
333
120
|
`Every other browser origin gets a 403.`);
|
|
334
121
|
const handler = (0, cors_1.default)({
|
|
335
122
|
origin: true, // reflect the request origin — we have already vetted it below
|
|
@@ -350,7 +137,7 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
350
137
|
handler(req, res, next);
|
|
351
138
|
return;
|
|
352
139
|
}
|
|
353
|
-
|
|
140
|
+
corsLog.info(`Blocked origin: ${origin}`);
|
|
354
141
|
res.status(403).json({
|
|
355
142
|
name: 'CorsError',
|
|
356
143
|
message: `CORS not allowed for origin: ${origin}`,
|
|
@@ -370,8 +157,8 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
370
157
|
originHost = new URL(origin).host;
|
|
371
158
|
}
|
|
372
159
|
catch (err) {
|
|
373
|
-
const error = (0,
|
|
374
|
-
|
|
160
|
+
const error = (0, core_util_1.toError)(err);
|
|
161
|
+
corsLog.info(`Malformed Origin header '${origin}': ${error.message}`);
|
|
375
162
|
return false;
|
|
376
163
|
}
|
|
377
164
|
if (host !== undefined && originHost === host) {
|
|
@@ -417,7 +204,7 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
417
204
|
createExpressWrapper(
|
|
418
205
|
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
419
206
|
clientMethod, path, formPost = false) {
|
|
420
|
-
return new ExpressWrapper(clientMethod, path, this.headers, formPost);
|
|
207
|
+
return new ExpressWrapper_1.ExpressWrapper(clientMethod, path, this.headers, formPost);
|
|
421
208
|
}
|
|
422
209
|
};
|
|
423
210
|
exports.WebpiecesMiddleware = WebpiecesMiddleware;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesMiddleware.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { Request, Response, NextFunction, RequestHandler } from 'express';\nimport cors from 'cors';\nimport { provideFrameworkSingleton, WebpiecesConfig } from '@webpieces/http-routing';\nimport {\n ProtocolError,\n ClientRegistry,\n HttpError,\n HttpBadRequestError,\n HttpVendorError,\n HttpUserError,\n HttpNotFoundError,\n HttpTimeoutError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpInternalServerError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n} from '@webpieces/core-util';\nimport { toError } from '@webpieces/core-util';\nimport { RequestContext, HttpRequest, RequestContextHeaders } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('WebpiecesMiddleware');\n\n/**\n * Express route handler function type. Lives in http-server (the express adapter),\n * NOT in the node-only http-routing package, so http-routing stays express-free.\n * Used by WebpiecesExpressRouter to register handlers Express can call.\n */\nexport type ExpressRouteHandler = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise<void>;\n\nexport class ExpressWrapper {\n constructor(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private clientMethod: (requestDto: unknown) => Promise<unknown>,\n private path: string,\n /** Owns the wire<->context transfer, both directions. Stateless framework singleton. */\n private headers: RequestContextHeaders,\n /**\n * True for an @Endpoint(..., { formPost: true }) route: parse the body as\n * application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not\n * the request Content-Type header — the annotation is the single source of truth.\n */\n private formPost: boolean = false,\n ) {\n }\n\n public async execute(req: Request, res: Response, next: NextFunction) {\n // MOVED: Wrap entire request in RequestContext.run()\n // This establishes AsyncLocalStorage context for the request\n await RequestContext.run(async () => {\n await this.executeTryCatch(req, res, next);\n });\n }\n\n public async executeTryCatch(req: Request, res: Response, next: NextFunction): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- ExpressWrapper catches errors to translate to HTTP responses\n try {\n await this.executeImpl(req, res, next);\n } catch (err: unknown) {\n const error = toError(err);\n // 5. Handle errors\n this.handleError(res, error);\n }\n }\n\n public async executeImpl(req: Request, res: Response, next: NextFunction): Promise<void> {\n // 1. Translate express's request into the transport-neutral HttpRequest webpieces speaks.\n const httpRequest = this.toWebpiecesRequest(req);\n\n // 2. Parse the request body. The PARSER is chosen by the @Endpoint annotation (this.formPost),\n // NOT the request Content-Type header — the annotation is the single source of truth.\n let requestDto: unknown = {};\n if (['POST', 'PUT', 'PATCH'].includes(req.method)) {\n const bodyText = await this.readRequestBody(req);\n if (this.formPost) {\n // application/x-www-form-urlencoded → flat key→value. URLSearchParams is lenient\n // (never throws) — right for EXTERNAL webhooks (e.g. Twilio) that post form-encoded.\n requestDto = Object.fromEntries(new URLSearchParams(bodyText));\n } else {\n // JSON (default, SYMMETRIC with the client's JSON.stringify). A non-JSON body is a\n // CLIENT error → 400, not the raw 500 an unguarded JSON.parse would throw.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- translate parse failure to a 400 HttpError\n try {\n requestDto = bodyText ? JSON.parse(bodyText) : {};\n } catch (err: unknown) {\n const error = toError(err);\n throw new HttpBadRequestError('Request body is not valid JSON', undefined, undefined, error);\n }\n }\n }\n\n // 3. Publish the transport-neutral HttpRequest, then move its headers into the context and\n // mint a request id if the caller sent none. BOTH happen above the api boundary, because\n // http-routing requires an already-established, already-filled request scope — it never\n // builds one for you. This is the \"translation layer\" every transport must provide.\n this.headers.fillFromRequest(httpRequest);\n\n // 4. Invoke the api CLIENT method — the SAME proxy tests use. Its filter chain + controller\n // run here, reading the context filled above; the chain never touches express `req`.\n const result = await this.clientMethod(requestDto);\n\n // 5. Serialize the response DTO to JSON (SYMMETRIC with client's response.json())\n const responseJson = JSON.stringify(result);\n res.status(200).setHeader('Content-Type', 'application/json').send(responseJson);\n }\n\n /**\n * Read HTTP headers from Express request.\n * Returns Map of header name (lowercase) -> array of values.\n *\n * HTTP spec allows multiple values for same header name.\n */\n /**\n * express Request -> webpieces {@link HttpRequest}. THE translation layer: below this line the\n * filter chain and controllers never see express, which is what lets the same chain run\n * in-process with no transport at all.\n */\n private toWebpiecesRequest(req: Request): HttpRequest {\n return new HttpRequest(req.method, this.path, this.readExpressHeaders(req));\n }\n\n private readExpressHeaders(req: Request): Map<string, string[]> {\n const headers = new Map<string, string[]>();\n\n // Express stores headers in req.headers as Record<string, string | string[]>\n for (const [name, value] of Object.entries(req.headers)) {\n const lowerName = name.toLowerCase();\n\n if (typeof value === 'string') {\n headers.set(lowerName, [value]);\n } else if (Array.isArray(value)) {\n headers.set(lowerName, value);\n }\n }\n\n return headers;\n }\n\n /**\n * Read raw request body as text.\n * Used to manually parse JSON (instead of express.json() middleware).\n */\n private async readRequestBody(req: Request): Promise<string> {\n return new Promise((resolve, reject) => {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk.toString();\n });\n req.on('end', () => {\n resolve(body);\n });\n req.on('error', (err) => {\n reject(err);\n });\n });\n }\n\n /**\n * Handle errors - translate to JSON ProtocolError (SYMMETRIC with ClientErrorTranslator).\n * PUBLIC so wrapExpress can call it for symmetric error handling.\n * Maps HttpError subclasses to appropriate HTTP status codes and ProtocolError response.\n *\n * Maps all HttpError types (must match ClientErrorTranslator.translateError()):\n * - HttpUserError → 266 (with errorCode)\n * - HttpBadRequestError → 400 (with field, guiAlertMessage)\n * - HttpUnauthorizedError → 401\n * - HttpForbiddenError → 403\n * - HttpNotFoundError → 404\n * - HttpTimeoutError → 408\n * - HttpInternalServerError → 500\n * - HttpBadGatewayError → 502\n * - HttpGatewayTimeoutError → 504\n * - HttpVendorError → 598 (with waitSeconds)\n */\n public handleError(res: Response, error: unknown): void {\n if (res.headersSent) {\n return;\n }\n\n // App-registered translations win, so an app can serialize its OWN error types (e.g. a\n // custom 460) AND override built-ins. `undefined` means \"not mine\" — fall through to the\n // built-in instanceof-HttpError ladder below, which stays the generic default. Symmetric\n // with the client's ClientErrorTranslator, which consults tryTranslateFromWire() first.\n if (error instanceof Error) {\n const wire = ClientRegistry.tryTranslateToWire(error);\n if (wire !== undefined) {\n res.status(wire.statusCode)\n .setHeader('Content-Type', 'application/json')\n .send(JSON.stringify(wire.protocolError));\n return;\n }\n }\n\n const protocolError = new ProtocolError();\n\n if (error instanceof HttpError) {\n // Set common fields for all HttpError types\n protocolError.message = error.message;\n protocolError.subType = error.subType;\n protocolError.name = error.name;\n\n // Set type-specific fields (MUST match ClientErrorTranslator)\n if (error instanceof HttpUserError) {\n log.info(`[ExpressWrapper] User Error: ${error.message}`);\n protocolError.errorCode = error.errorCode;\n } else if (error instanceof HttpBadRequestError) {\n log.info(`[ExpressWrapper] Bad Request: ${error.message}`);\n protocolError.field = error.field;\n protocolError.guiAlertMessage = error.guiMessage;\n } else if (error instanceof HttpNotFoundError) {\n log.info(`[ExpressWrapper] Not Found: ${error.message}`);\n } else if (error instanceof HttpTimeoutError) {\n log.error(`[ExpressWrapper] Timeout Error: ${error.message}`);\n } else if (error instanceof HttpVendorError) {\n log.error(`[ExpressWrapper] Vendor Error: ${error.message}`);\n protocolError.waitSeconds = error.waitSeconds;\n } else if (error instanceof HttpUnauthorizedError) {\n log.info(`[ExpressWrapper] Unauthorized: ${error.message}`);\n } else if (error instanceof HttpForbiddenError) {\n log.info(`[ExpressWrapper] Forbidden: ${error.message}`);\n } else if (error instanceof HttpInternalServerError) {\n log.error(`[ExpressWrapper] Internal Server Error: ${error.message}`);\n } else if (error instanceof HttpBadGatewayError) {\n log.error(`[ExpressWrapper] Bad Gateway: ${error.message}`);\n } else if (error instanceof HttpGatewayTimeoutError) {\n log.error(`[ExpressWrapper] Gateway Timeout: ${error.message}`);\n } else {\n log.info(`[ExpressWrapper] Generic HttpError: ${error.message}`);\n }\n\n // Serialize ProtocolError to JSON (SYMMETRIC with client)\n const responseJson = JSON.stringify(protocolError);\n res.status(error.code).setHeader('Content-Type', 'application/json').send(responseJson);\n } else {\n // Unknown error - 500\n const err = toError(error);\n protocolError.message = 'Internal Server Error';\n log.error('[ExpressWrapper] Unexpected error:', err);\n const responseJson = JSON.stringify(protocolError);\n res.status(500).setHeader('Content-Type', 'application/json').send(responseJson);\n }\n }\n}\n\n/**\n * WebpiecesMiddleware - Express middleware for WebPieces server.\n *\n * This class contains all Express middleware used by WebpiecesServer:\n * 1. globalErrorHandler - Outermost error handler, returns HTML 500 page\n * 2. logNextLayer - Request/response logging\n * 3. jsonTranslator - JSON Content-Type validation and error translation\n *\n * The middleware is injected into WebpiecesServerImpl and registered with Express\n * in the start() method.\n *\n * IMPORTANT: jsonTranslator does NOT dispatch routes - route dispatch happens via\n * Express's registered route handlers (created by RouteBuilder.createHandler()).\n * jsonTranslator only validates Content-Type and translates errors to JSON.\n *\n * NEW: ExpressWrapper simplified - no longer handles JSON or headers\n * - JSON parsing/serialization moved to JsonFilter\n * - Header transfer moved to ContextFilter (injects PlatformHeadersExtension directly)\n * - ExpressWrapper just creates RouterReqResp and invokes filter chain\n *\n * Extension vs Plugin pattern:\n * - Extensions (DI-level): Contribute capabilities to framework (headers, converters, etc.)\n * - Plugins (App-level): Provide complete features with modules + routes (Hibernate, Jackson, etc.)\n */\n@provideFrameworkSingleton()\nexport class WebpiecesMiddleware {\n /** The ONE wire<->context transfer, handed to every route's ExpressWrapper. Stateless. */\n private readonly headers = new RequestContextHeaders();\n\n\n /**\n * Global error handler middleware - catches ALL unhandled errors.\n * Returns HTML 500 error page for any errors that escape the filter chain.\n *\n * This is the outermost safety net - JsonTranslator catches JSON API errors,\n * this catches everything else.\n */\n async globalErrorHandler(\n req: Request,\n res: Response,\n next: NextFunction,\n ): Promise<void> {\n log.info(`🔴 [Layer 1: GlobalErrorHandler] Request START: ${req.method} ${req.path}`);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Global error handler IS the top-level catch-all\n try {\n // await next() catches BOTH:\n // 1. Synchronous throws from next() itself\n // 2. Rejected promises from downstream async middleware\n await next();\n log.info(\n `🔴 [Layer 1: GlobalErrorHandler] Request END (success): ${req.method} ${req.path}`,\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.error('🔴 [Layer 1: GlobalErrorHandler] Caught unhandled error:', error);\n if (!res.headersSent) {\n // Return HTML error page (not JSON - JsonTranslator handles JSON errors)\n res.status(500).send(`\n <!DOCTYPE html>\n <html>\n <head><title>Server Error</title></head>\n <body>\n <h1>You hit a server error</h1>\n <p>An unexpected error occurred while processing your request.</p>\n <pre>${error.message}</pre>\n </body>\n </html>\n `);\n }\n log.info(\n `🔴 [Layer 1: GlobalErrorHandler] Request END (error): ${req.method} ${req.path}`,\n );\n }\n }\n\n /**\n * Logging middleware - logs request/response flow.\n * Demonstrates middleware execution order.\n * IMPORTANT: Must be async and await next() to properly chain with async middleware.\n */\n async logNextLayer(req: Request, res: Response, next: NextFunction): Promise<void> {\n log.info(`🟡 [Layer 2: LogNextLayer] Before next() - ${req.method} ${req.path}`);\n await next();\n log.info(`🟡 [Layer 2: LogNextLayer] After next() - ${req.method} ${req.path}`);\n }\n\n /**\n * CORS middleware. DO NOT MOUNT UNCONDITIONALLY — {@link WebpiecesExpressRouter.bindAndStartExpress}\n * mounts it ONLY when {@link WebpiecesConfig.corsOrigins} is non-empty, and that is the point.\n *\n * CORS exists solely to let a browser on a DIFFERENT origin call this api — in practice\n * `ng serve` on :4200 hitting an api on :8080 during development, or a UI hosted on a different\n * host than the api. A server that serves its own browser app needs NO cors at all, because a\n * browser does not apply cors to a same-origin request. So in production this middleware is\n * normally ABSENT, and absent is the safe state: every origin it allows gains the right to make\n * CREDENTIALED cross-origin calls and READ the responses. Mounting it unconditionally (as the\n * old corsForLocalhost did) handed that right to anything on the victim's localhost, in prod,\n * for no benefit whatsoever.\n *\n * When mounted, allows: a request with NO Origin (curl, server-to-server, a CLI); the server's\n * OWN origin; and EXACTLY the origins in `corsOrigins` — nothing is implicit. Anything else gets\n * a clean 403, never the HTML 500 the old `callback(new Error(...))` produced.\n *\n * SAME-ORIGIN MUST STAY ALLOWED even though a same-origin request needs no cors headers, because\n * a browser attaches an `Origin` header to EVERY POST — including a same-origin POST — and every\n * webpieces route is a POST. Once mounted, this middleware SEES that origin, so if it did not\n * allow it, it would 403 the server's own UI. That was the production bug.\n *\n * The same-origin test compares HOST ONLY, deliberately. Behind a TLS-terminating proxy (Cloud\n * Run, any load balancer) `req.protocol` is `http` while the browser's `Origin` says `https`, so\n * comparing full origins would reject the server's own origin on every deploy.\n *\n * @returns Express middleware handler for CORS\n */\n corsMiddleware(config?: WebpiecesConfig): RequestHandler {\n const allowedOrigins = config?.corsOrigins ?? [];\n log.info(\n `[WebpiecesMiddleware] CORS MOUNTED. Allowing same-origin + [${allowedOrigins.join(', ')}]. ` +\n `Every other browser origin gets a 403.`,\n );\n\n const handler = cors({\n origin: true, // reflect the request origin — we have already vetted it below\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n allowedHeaders: '*',\n exposedHeaders: '*',\n maxAge: 3600,\n });\n\n return (req: Request, res: Response, next: NextFunction): void => {\n const origin = req.headers.origin;\n if (!origin) {\n // No Origin -> not a browser cross-origin request; nothing to negotiate.\n next();\n return;\n }\n if (this.isOriginAllowed(origin, req.get('host'), allowedOrigins)) {\n handler(req, res, next);\n return;\n }\n log.info(`[CORS] Blocked origin: ${origin}`);\n res.status(403).json({\n name: 'CorsError',\n message: `CORS not allowed for origin: ${origin}`,\n });\n };\n }\n\n /**\n * Same-origin (HOST ONLY — see corsMiddleware() on why the scheme is deliberately ignored), or an\n * explicit entry in corsOrigins. NOTHING is implicit: localhost is allowed only if the config\n * asked for it, so a production server that enables cors for a cross-host UI does not silently\n * open the door to localhost as well.\n */\n private isOriginAllowed(origin: string, host: string | undefined, allowedOrigins: string[]): boolean {\n let originHost: string;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed Origin is untrusted browser input, not a server fault; it must become a 403 here, never bubble to the 500 chokepoint\n try {\n originHost = new URL(origin).host;\n } catch (err: unknown) {\n const error = toError(err);\n log.info(`[CORS] Malformed Origin header '${origin}': ${error.message}`);\n return false;\n }\n if (host !== undefined && originHost === host) {\n return true; // same-origin: the server's own UI calling its own api\n }\n return allowedOrigins.some((allowed: string): boolean => this.matchesOrigin(origin, allowed));\n }\n\n /**\n * Exact origin match, except a `*` in the PORT position matches any port: `http://localhost:*`\n * is what a developer writes, because the angular dev-server port moves around.\n *\n * The `*` is deliberately NOT a general wildcard — it never spans a host, and what follows the\n * prefix must be a real (digits-only) port. So `http://localhost:*` cannot be tricked into\n * matching `http://localhost.evil.com`, and a bare `*` matches nothing at all.\n */\n private matchesOrigin(origin: string, allowed: string): boolean {\n if (allowed === origin) {\n return true;\n }\n const wildcardSuffix = ':*';\n if (!allowed.endsWith(wildcardSuffix)) {\n return false;\n }\n const prefix = allowed.slice(0, -wildcardSuffix.length);\n if (!origin.startsWith(`${prefix}:`)) {\n return false;\n }\n const port = origin.slice(prefix.length + 1);\n return /^\\d+$/.test(port);\n }\n\n /**\n * Create an ExpressWrapper for a route.\n * The wrapper handles the full request/response cycle (symmetric design): it publishes the\n * HttpRequest + fills the context, then invokes the api client method (the proxy).\n *\n * @param clientMethod - The api client's method for this route (dto → response); the proxy\n * runs the filter chain + controller.\n * @param path - The route path (used to build the HttpRequest).\n * @param formPost - True for an @Endpoint(..., { formPost: true }) route (parse body as\n * urlencoded, not JSON). Default false = JSON.\n * @returns ExpressWrapper instance\n */\n createExpressWrapper(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n clientMethod: (requestDto: unknown) => Promise<unknown>,\n path: string,\n formPost: boolean = false,\n ): ExpressWrapper {\n return new ExpressWrapper(clientMethod, path, this.headers, formPost);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesMiddleware.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { Request, Response, NextFunction, RequestHandler } from 'express';\nimport cors from 'cors';\nimport { provideFrameworkSingleton, WebpiecesConfig } from '@webpieces/http-routing';\nimport { toError } from '@webpieces/core-util';\nimport { RequestContextHeaders } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport { ExpressWrapper } from './ExpressWrapper';\n\nconst log = LogManager.getLogger('WebpiecesMiddleware');\n// CORS mount/allow/block lines log under their own name (not [WebpiecesMiddleware]); the backend\n// prepends \"[CORS]\" for us, so the message strings below carry no literal prefix of their own.\nconst corsLog = LogManager.getLogger('CORS');\n\n/**\n * Express route handler function type. Lives in http-server (the express adapter),\n * NOT in the node-only http-routing package, so http-routing stays express-free.\n * Used by WebpiecesExpressRouter to register handlers Express can call.\n */\nexport type ExpressRouteHandler = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise<void>;\n\n/**\n * WebpiecesMiddleware - Express middleware for WebPieces server.\n *\n * This class contains all Express middleware used by WebpiecesServer:\n * 1. globalErrorHandler - Outermost error handler, returns HTML 500 page\n * 2. logNextLayer - Request/response logging\n * 3. jsonTranslator - JSON Content-Type validation and error translation\n *\n * The middleware is injected into WebpiecesServerImpl and registered with Express\n * in the start() method.\n *\n * IMPORTANT: jsonTranslator does NOT dispatch routes - route dispatch happens via\n * Express's registered route handlers (created by RouteBuilder.createHandler()).\n * jsonTranslator only validates Content-Type and translates errors to JSON.\n *\n * NEW: ExpressWrapper simplified - no longer handles JSON or headers\n * - JSON parsing/serialization moved to JsonFilter\n * - Header transfer moved to ContextFilter (injects PlatformHeadersExtension directly)\n * - ExpressWrapper just creates RouterReqResp and invokes filter chain\n *\n * Extension vs Plugin pattern:\n * - Extensions (DI-level): Contribute capabilities to framework (headers, converters, etc.)\n * - Plugins (App-level): Provide complete features with modules + routes (Hibernate, Jackson, etc.)\n */\n@provideFrameworkSingleton()\nexport class WebpiecesMiddleware {\n /** The ONE wire<->context transfer, handed to every route's ExpressWrapper. Stateless. */\n private readonly headers = new RequestContextHeaders();\n\n\n /**\n * Global error handler middleware - catches ALL unhandled errors.\n * Returns HTML 500 error page for any errors that escape the filter chain.\n *\n * This is the outermost safety net - JsonTranslator catches JSON API errors,\n * this catches everything else.\n */\n async globalErrorHandler(\n req: Request,\n res: Response,\n next: NextFunction,\n ): Promise<void> {\n log.info(`🔴 [Layer 1: GlobalErrorHandler] Request START: ${req.method} ${req.path}`);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Global error handler IS the top-level catch-all\n try {\n // await next() catches BOTH:\n // 1. Synchronous throws from next() itself\n // 2. Rejected promises from downstream async middleware\n await next();\n log.info(\n `🔴 [Layer 1: GlobalErrorHandler] Request END (success): ${req.method} ${req.path}`,\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.error('🔴 [Layer 1: GlobalErrorHandler] Caught unhandled error:', error);\n if (!res.headersSent) {\n // Return HTML error page (not JSON - JsonTranslator handles JSON errors)\n res.status(500).send(`\n <!DOCTYPE html>\n <html>\n <head><title>Server Error</title></head>\n <body>\n <h1>You hit a server error</h1>\n <p>An unexpected error occurred while processing your request.</p>\n <pre>${error.message}</pre>\n </body>\n </html>\n `);\n }\n log.info(\n `🔴 [Layer 1: GlobalErrorHandler] Request END (error): ${req.method} ${req.path}`,\n );\n }\n }\n\n /**\n * Logging middleware - logs request/response flow.\n * Demonstrates middleware execution order.\n * IMPORTANT: Must be async and await next() to properly chain with async middleware.\n */\n async logNextLayer(req: Request, res: Response, next: NextFunction): Promise<void> {\n log.info(`🟡 [Layer 2: LogNextLayer] Before next() - ${req.method} ${req.path}`);\n await next();\n log.info(`🟡 [Layer 2: LogNextLayer] After next() - ${req.method} ${req.path}`);\n }\n\n /**\n * CORS middleware. DO NOT MOUNT UNCONDITIONALLY — {@link WebpiecesExpressRouter.bindAndStartExpress}\n * mounts it ONLY when {@link WebpiecesConfig.corsOrigins} is non-empty, and that is the point.\n *\n * CORS exists solely to let a browser on a DIFFERENT origin call this api — in practice\n * `ng serve` on :4200 hitting an api on :8080 during development, or a UI hosted on a different\n * host than the api. A server that serves its own browser app needs NO cors at all, because a\n * browser does not apply cors to a same-origin request. So in production this middleware is\n * normally ABSENT, and absent is the safe state: every origin it allows gains the right to make\n * CREDENTIALED cross-origin calls and READ the responses. Mounting it unconditionally (as the\n * old corsForLocalhost did) handed that right to anything on the victim's localhost, in prod,\n * for no benefit whatsoever.\n *\n * When mounted, allows: a request with NO Origin (curl, server-to-server, a CLI); the server's\n * OWN origin; and EXACTLY the origins in `corsOrigins` — nothing is implicit. Anything else gets\n * a clean 403, never the HTML 500 the old `callback(new Error(...))` produced.\n *\n * SAME-ORIGIN MUST STAY ALLOWED even though a same-origin request needs no cors headers, because\n * a browser attaches an `Origin` header to EVERY POST — including a same-origin POST — and every\n * webpieces route is a POST. Once mounted, this middleware SEES that origin, so if it did not\n * allow it, it would 403 the server's own UI. That was the production bug.\n *\n * The same-origin test compares HOST ONLY, deliberately. Behind a TLS-terminating proxy (Cloud\n * Run, any load balancer) `req.protocol` is `http` while the browser's `Origin` says `https`, so\n * comparing full origins would reject the server's own origin on every deploy.\n *\n * @returns Express middleware handler for CORS\n */\n corsMiddleware(config?: WebpiecesConfig): RequestHandler {\n const allowedOrigins = config?.corsOrigins ?? [];\n corsLog.info(\n `CORS MOUNTED. Allowing same-origin + [${allowedOrigins.join(', ')}]. ` +\n `Every other browser origin gets a 403.`,\n );\n\n const handler = cors({\n origin: true, // reflect the request origin — we have already vetted it below\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n allowedHeaders: '*',\n exposedHeaders: '*',\n maxAge: 3600,\n });\n\n return (req: Request, res: Response, next: NextFunction): void => {\n const origin = req.headers.origin;\n if (!origin) {\n // No Origin -> not a browser cross-origin request; nothing to negotiate.\n next();\n return;\n }\n if (this.isOriginAllowed(origin, req.get('host'), allowedOrigins)) {\n handler(req, res, next);\n return;\n }\n corsLog.info(`Blocked origin: ${origin}`);\n res.status(403).json({\n name: 'CorsError',\n message: `CORS not allowed for origin: ${origin}`,\n });\n };\n }\n\n /**\n * Same-origin (HOST ONLY — see corsMiddleware() on why the scheme is deliberately ignored), or an\n * explicit entry in corsOrigins. NOTHING is implicit: localhost is allowed only if the config\n * asked for it, so a production server that enables cors for a cross-host UI does not silently\n * open the door to localhost as well.\n */\n private isOriginAllowed(origin: string, host: string | undefined, allowedOrigins: string[]): boolean {\n let originHost: string;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed Origin is untrusted browser input, not a server fault; it must become a 403 here, never bubble to the 500 chokepoint\n try {\n originHost = new URL(origin).host;\n } catch (err: unknown) {\n const error = toError(err);\n corsLog.info(`Malformed Origin header '${origin}': ${error.message}`);\n return false;\n }\n if (host !== undefined && originHost === host) {\n return true; // same-origin: the server's own UI calling its own api\n }\n return allowedOrigins.some((allowed: string): boolean => this.matchesOrigin(origin, allowed));\n }\n\n /**\n * Exact origin match, except a `*` in the PORT position matches any port: `http://localhost:*`\n * is what a developer writes, because the angular dev-server port moves around.\n *\n * The `*` is deliberately NOT a general wildcard — it never spans a host, and what follows the\n * prefix must be a real (digits-only) port. So `http://localhost:*` cannot be tricked into\n * matching `http://localhost.evil.com`, and a bare `*` matches nothing at all.\n */\n private matchesOrigin(origin: string, allowed: string): boolean {\n if (allowed === origin) {\n return true;\n }\n const wildcardSuffix = ':*';\n if (!allowed.endsWith(wildcardSuffix)) {\n return false;\n }\n const prefix = allowed.slice(0, -wildcardSuffix.length);\n if (!origin.startsWith(`${prefix}:`)) {\n return false;\n }\n const port = origin.slice(prefix.length + 1);\n return /^\\d+$/.test(port);\n }\n\n /**\n * Create an ExpressWrapper for a route.\n * The wrapper handles the full request/response cycle (symmetric design): it publishes the\n * HttpRequest + fills the context, then invokes the api client method (the proxy).\n *\n * @param clientMethod - The api client's method for this route (dto → response); the proxy\n * runs the filter chain + controller.\n * @param path - The route path (used to build the HttpRequest).\n * @param formPost - True for an @Endpoint(..., { formPost: true }) route (parse body as\n * urlencoded, not JSON). Default false = JSON.\n * @returns ExpressWrapper instance\n */\n createExpressWrapper(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n clientMethod: (requestDto: unknown) => Promise<unknown>,\n path: string,\n formPost: boolean = false,\n ): ExpressWrapper {\n return new ExpressWrapper(clientMethod, path, this.headers, formPost);\n }\n}\n"]}
|
|
@@ -49,7 +49,7 @@ class TestCaseRecorderImpl {
|
|
|
49
49
|
const fixtureJson = this.serializer.serialize(testCase);
|
|
50
50
|
const baseName = this.buildBaseName(serverEndpoint, testCase.recordedAt);
|
|
51
51
|
const specSource = this.specGenerator.generate(testCase, `${baseName}.fixture.json`);
|
|
52
|
-
log.info(`
|
|
52
|
+
log.info(`Recorded ${serverEndpoint.apiName}.${serverEndpoint.methodName} ` +
|
|
53
53
|
`(${this.downstreamCalls.length} downstream calls)\n` +
|
|
54
54
|
`--- fixture (${baseName}.fixture.json) ---\n${fixtureJson}\n` +
|
|
55
55
|
`--- generated spec (${baseName}.spec.ts) ---\n${specSource}`);
|
|
@@ -57,13 +57,13 @@ class TestCaseRecorderImpl {
|
|
|
57
57
|
fs.mkdirSync(recordingDir, { recursive: true });
|
|
58
58
|
fs.writeFileSync(path.join(recordingDir, `${baseName}.fixture.json`), fixtureJson);
|
|
59
59
|
fs.writeFileSync(path.join(recordingDir, `${baseName}.spec.ts`), specSource);
|
|
60
|
-
log.info(`
|
|
60
|
+
log.info(`Wrote fixture + spec to ${recordingDir}/${baseName}.*`);
|
|
61
61
|
}
|
|
62
62
|
return testCase;
|
|
63
63
|
}
|
|
64
64
|
catch (err) {
|
|
65
65
|
const error = (0, core_util_2.toError)(err);
|
|
66
|
-
log.error('
|
|
66
|
+
log.error('Failed to emit test case (request unaffected)', error);
|
|
67
67
|
return undefined;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestCaseRecorderImpl.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/recorder/TestCaseRecorderImpl.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,oDAK8B;AAC9B,oDAA+C;AAC/C,mDAAgD;AAChD,oDAAkD;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;AAErD,MAAa,oBAAoB;IACrB,eAAe,GAAuB,EAAE,CAAC;IACzC,UAAU,GAAG,IAAI,4BAAgB,EAAE,CAAC;IACpC,aAAa,GAAG,IAAI,6BAAa,EAAE,CAAC;IAE5C,eAAe,CAAC,IAAsB;QAClC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,mBAAmB;QACf,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACjE,CAAC;IAED;;;;;;OAMG;IACH,eAAe,CAAC,cAAgC,EAAE,YAAqB;QACnE,gHAAgH;QAChH,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,4BAAgB,CAAC,cAAc,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YACtG,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YAExD,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;YACzE,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,QAAQ,eAAe,CAAC,CAAC;YAErF,GAAG,CAAC,IAAI,CAAC
|
|
1
|
+
{"version":3,"file":"TestCaseRecorderImpl.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/recorder/TestCaseRecorderImpl.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,oDAK8B;AAC9B,oDAA+C;AAC/C,mDAAgD;AAChD,oDAAkD;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;AAErD,MAAa,oBAAoB;IACrB,eAAe,GAAuB,EAAE,CAAC;IACzC,UAAU,GAAG,IAAI,4BAAgB,EAAE,CAAC;IACpC,aAAa,GAAG,IAAI,6BAAa,EAAE,CAAC;IAE5C,eAAe,CAAC,IAAsB;QAClC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,mBAAmB;QACf,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACjE,CAAC;IAED;;;;;;OAMG;IACH,eAAe,CAAC,cAAgC,EAAE,YAAqB;QACnE,gHAAgH;QAChH,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,4BAAgB,CAAC,cAAc,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YACtG,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YAExD,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;YACzE,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,QAAQ,eAAe,CAAC,CAAC;YAErF,GAAG,CAAC,IAAI,CAAC,YAAY,cAAc,CAAC,OAAO,IAAI,cAAc,CAAC,UAAU,GAAG;gBACvE,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,sBAAsB;gBACrD,gBAAgB,QAAQ,uBAAuB,WAAW,IAAI;gBAC9D,uBAAuB,QAAQ,kBAAkB,UAAU,EAAE,CAAC,CAAC;YAEnE,IAAI,YAAY,EAAE,CAAC;gBACf,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAChD,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,QAAQ,eAAe,CAAC,EAAE,WAAW,CAAC,CAAC;gBACnF,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,QAAQ,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;gBAC7E,GAAG,CAAC,IAAI,CAAC,2BAA2B,YAAY,IAAI,QAAQ,IAAI,CAAC,CAAC;YACtE,CAAC;YAED,OAAO,QAAQ,CAAC;QACpB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,GAAG,CAAC,KAAK,CAAC,+CAA+C,EAAE,KAAK,CAAC,CAAC;YAClE,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IAEO,aAAa,CAAC,cAAgC,EAAE,UAAkB;QACtE,oEAAoE;QACpE,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACjE,OAAO,GAAG,cAAc,CAAC,OAAO,IAAI,cAAc,CAAC,UAAU,IAAI,KAAK,EAAE,CAAC;IAC7E,CAAC;CACJ;AAtDD,oDAsDC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n RecordedEndpoint,\n RecordedTestCase,\n RecordSerializer,\n TestCaseRecorder,\n} from '@webpieces/core-util';\nimport { toError } from '@webpieces/core-util';\nimport { SpecGenerator } from './SpecGenerator';\nimport { LogManager } from '@webpieces/core-util';\n\n/**\n * TestCaseRecorderImpl - Server-side recorder (port of Java\n * TestCaseRecorderImpl, minus the fragile bean-reflection codegen).\n *\n * One instance per recorded request (created by RecordingFilter and placed in\n * the RequestContext under RecorderKeys.RECORDER). Downstream hooks - the\n * http-client proxy and recordable() wrappers - add every call they make.\n *\n * spitOutTestCase() emits:\n * - a diffable JSON FIXTURE (the stable artifact: request, ctx snapshot,\n * response, all downstream calls) - also perfect input for an AI to write\n * a richer spec from\n * - a small deterministic .spec.ts from SpecGenerator\n * Both are always logged; written to recordingDir when configured.\n * NEVER breaks production - the whole body is caught and logged.\n */\nconst log = LogManager.getLogger('TestCaseRecorder');\n\nexport class TestCaseRecorderImpl implements TestCaseRecorder {\n private downstreamCalls: RecordedEndpoint[] = [];\n private serializer = new RecordSerializer();\n private specGenerator = new SpecGenerator();\n\n addEndpointInfo(info: RecordedEndpoint): void {\n this.downstreamCalls.push(info);\n }\n\n getLastEndpointInfo(): RecordedEndpoint | undefined {\n return this.downstreamCalls[this.downstreamCalls.length - 1];\n }\n\n /**\n * Emit the recorded test case. Called by RecordingFilter in its finally.\n *\n * @param serverEndpoint - The inbound endpoint capture (request + response)\n * @param recordingDir - Directory to write fixture + spec files (optional)\n * @returns The built RecordedTestCase, or undefined if emission failed\n */\n spitOutTestCase(serverEndpoint: RecordedEndpoint, recordingDir?: string): RecordedTestCase | undefined {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- recording must NEVER break production requests\n try {\n const testCase = new RecordedTestCase(serverEndpoint, this.downstreamCalls, new Date().toISOString());\n const fixtureJson = this.serializer.serialize(testCase);\n\n const baseName = this.buildBaseName(serverEndpoint, testCase.recordedAt);\n const specSource = this.specGenerator.generate(testCase, `${baseName}.fixture.json`);\n\n log.info(`Recorded ${serverEndpoint.apiName}.${serverEndpoint.methodName} ` +\n `(${this.downstreamCalls.length} downstream calls)\\n` +\n `--- fixture (${baseName}.fixture.json) ---\\n${fixtureJson}\\n` +\n `--- generated spec (${baseName}.spec.ts) ---\\n${specSource}`);\n\n if (recordingDir) {\n fs.mkdirSync(recordingDir, { recursive: true });\n fs.writeFileSync(path.join(recordingDir, `${baseName}.fixture.json`), fixtureJson);\n fs.writeFileSync(path.join(recordingDir, `${baseName}.spec.ts`), specSource);\n log.info(`Wrote fixture + spec to ${recordingDir}/${baseName}.*`);\n }\n\n return testCase;\n } catch (err: unknown) {\n const error = toError(err);\n log.error('Failed to emit test case (request unaffected)', error);\n return undefined;\n }\n }\n\n private buildBaseName(serverEndpoint: RecordedEndpoint, recordedAt: string): string {\n // 2026-07-04T10:22:33.123Z -> 2026-07-04T10-22-33 (filesystem-safe)\n const stamp = recordedAt.replace(/:/g, '-').replace(/\\..*$/, '');\n return `${serverEndpoint.apiName}.${serverEndpoint.methodName}.${stamp}`;\n }\n}\n"]}
|