@webpieces/http-server 0.4.402 → 0.4.404

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-server",
3
- "version": "0.4.402",
3
+ "version": "0.4.404",
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.402",
26
- "@webpieces/core-util": "0.4.402",
27
- "@webpieces/gcp-identity": "0.4.402",
28
- "@webpieces/http-routing": "0.4.402",
25
+ "@webpieces/core-context": "0.4.404",
26
+ "@webpieces/core-util": "0.4.404",
27
+ "@webpieces/gcp-identity": "0.4.404",
28
+ "@webpieces/http-routing": "0.4.404",
29
29
  "cors": "2.8.5",
30
30
  "express": "5.1.0",
31
31
  "inversify": "7.10.4"
@@ -36,14 +36,20 @@ export declare class WebpiecesExpressRouter {
36
36
  */
37
37
  bindExpress(app: Express): void;
38
38
  /**
39
- * Add the webpieces global middleware (HTML error page, optional CORS, request logging), bind
40
- * the routes, then app.listen(port). Convenience for a non-legacy webpieces server where
39
+ * Add the webpieces global middleware (optional CORS), bind the routes, mount the top-level
40
+ * error handler AFTER them, then app.listen(port). Convenience for a non-legacy webpieces server where
41
41
  * webpieces owns the whole express app. Resolves with the http.Server once listening.
42
42
  *
43
43
  * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and
44
44
  * {@link WebpiecesMiddleware.corsMiddleware}.
45
45
  */
46
46
  bindAndStartExpress(app: Express, port?: number, config?: WebpiecesConfig): Promise<HttpServer>;
47
+ /**
48
+ * The "Svr Ready!!" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and
49
+ * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied
50
+ * verbatim from the trytami service so a familiar splash marks "the server is up and reachable".
51
+ */
52
+ private logStartupBanner;
47
53
  /**
48
54
  * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;
49
55
  * for each we wrap the matching client method (the proxy — RequestContext.run + header read +
@@ -47,8 +47,8 @@ class WebpiecesExpressRouter {
47
47
  log.info(`Mounted ${count} webpieces route(s) onto express`);
48
48
  }
49
49
  /**
50
- * Add the webpieces global middleware (HTML error page, optional CORS, request logging), bind
51
- * the routes, then app.listen(port). Convenience for a non-legacy webpieces server where
50
+ * Add the webpieces global middleware (optional CORS), bind the routes, mount the top-level
51
+ * error handler AFTER them, then app.listen(port). Convenience for a non-legacy webpieces server where
52
52
  * webpieces owns the whole express app. Resolves with the http.Server once listening.
53
53
  *
54
54
  * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and
@@ -56,7 +56,6 @@ class WebpiecesExpressRouter {
56
56
  */
57
57
  async bindAndStartExpress(app, port = 8080, config) {
58
58
  // Global middleware layers (outermost first) — only for a webpieces-owned app.
59
- app.use(this.middleware.globalErrorHandler.bind(this.middleware));
60
59
  // CORS is OPT-IN, and stays OFF in production. A server that serves its own browser app does
61
60
  // not need it — a browser applies no cors check to a same-origin request — so mounting it
62
61
  // would only hand credentialed cross-origin read access to whatever it allows, for nothing.
@@ -66,8 +65,11 @@ class WebpiecesExpressRouter {
66
65
  if (corsOrigins.length > 0) {
67
66
  app.use(this.middleware.corsMiddleware(config));
68
67
  }
69
- app.use(this.middleware.logNextLayer.bind(this.middleware));
70
68
  this.bindExpress(app);
69
+ // Top-level error handler is mounted LAST (AFTER the routes). Express only forwards a
70
+ // downstream failure to a 4-arg error middleware that sits BELOW the failing route — it does
71
+ // NOT bubble errors back up through next(). See WebpiecesMiddleware.errorHandler.
72
+ app.use(this.middleware.errorHandler.bind(this.middleware));
71
73
  return new Promise((resolve, reject) => {
72
74
  const server = app.listen(port, (error) => {
73
75
  if (error) {
@@ -76,10 +78,33 @@ class WebpiecesExpressRouter {
76
78
  return;
77
79
  }
78
80
  log.info(`Listening on http://localhost:${port}`);
81
+ this.logStartupBanner(port);
79
82
  resolve(server);
80
83
  });
81
84
  });
82
85
  }
86
+ /**
87
+ * The "Svr Ready!!" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and
88
+ * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied
89
+ * verbatim from the trytami service so a familiar splash marks "the server is up and reachable".
90
+ */
91
+ logStartupBanner(port) {
92
+ if (process.env['K_SERVICE']) {
93
+ return;
94
+ }
95
+ log.info(`
96
+ ___ _____ _
97
+ / _| | _ \\ | |
98
+ \\ \`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _
99
+ \`--. \\/ _ \\ '_\\ \\ / / _ \\ '_| | // _ \\/ _\` |/ _\` | | | |
100
+ /\\_/ / _/ | \\ V / _/ | | |\\ \\ _/ (_| | (_| | |_| |
101
+ \\___/ \\_|_| \\_/ \\_|_| \\_| \\_\\_|\\_,_|\\_,_|\\_, |
102
+ _/ |
103
+ |_/
104
+
105
+ Svr Ready!! port=${port}
106
+ `);
107
+ }
83
108
  /**
84
109
  * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;
85
110
  * for each we wrap the matching client method (the proxy — RequestContext.run + header read +
@@ -1 +1 @@
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
+ {"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 (optional CORS), bind the routes, mount the top-level\n * error handler AFTER them, 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 // 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 this.bindExpress(app);\n\n // Top-level error handler is mounted LAST (AFTER the routes). Express only forwards a\n // downstream failure to a 4-arg error middleware that sits BELOW the failing route — it does\n // NOT bubble errors back up through next(). See WebpiecesMiddleware.errorHandler.\n app.use(this.middleware.errorHandler.bind(this.middleware));\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 this.logStartupBanner(port);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * The \"Svr Ready!!\" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and\n * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied\n * verbatim from the trytami service so a familiar splash marks \"the server is up and reachable\".\n */\n private logStartupBanner(port: number): void {\n if (process.env['K_SERVICE']) {\n return;\n }\n log.info(`\n ___ _____ _\n/ _| | _ \\\\ | |\n\\\\ \\`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _\n \\`--. \\\\/ _ \\\\ '_\\\\ \\\\ / / _ \\\\ '_| | // _ \\\\/ _\\` |/ _\\` | | | |\n/\\\\_/ / _/ | \\\\ V / _/ | | |\\\\ \\\\ _/ (_| | (_| | |_| |\n\\\\___/ \\\\_|_| \\\\_/ \\\\_|_| \\\\_| \\\\_\\\\_|\\\\_,_|\\\\_,_|\\\\_, |\n _/ |\n |_/\n\n Svr Ready!! port=${port}\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"]}
@@ -11,16 +11,18 @@ export type ExpressRouteHandler = (req: Request, res: Response, next: NextFuncti
11
11
  * WebpiecesMiddleware - Express middleware for WebPieces server.
12
12
  *
13
13
  * This class contains all Express middleware used by WebpiecesServer:
14
- * 1. globalErrorHandler - Outermost error handler, returns HTML 500 page
15
- * 2. logNextLayer - Request/response logging
16
- * 3. jsonTranslator - JSON Content-Type validation and error translation
14
+ * 1. errorHandler - Top-level 4-arg error middleware, returns HTML 500 page. Mounted AFTER the
15
+ * routes so express can forward downstream failures to it (express routes errors DOWN a
16
+ * separate pipeline, it does not bubble them back up through next()). Last-resort net only:
17
+ * api routes translate their own errors to JSON inside the filter chain (ExpressWrapper).
18
+ * 2. corsMiddleware - Opt-in CORS (mounted only when corsOrigins is non-empty).
17
19
  *
18
- * The middleware is injected into WebpiecesServerImpl and registered with Express
19
- * in the start() method.
20
+ * Per-request logging is intentionally NOT here the api filter chain's LogApiCall already logs
21
+ * every request/response with full context (requestId, method, path, body), so a plain express
22
+ * START/END line would only duplicate it with less information.
20
23
  *
21
- * IMPORTANT: jsonTranslator does NOT dispatch routes - route dispatch happens via
22
- * Express's registered route handlers (created by RouteBuilder.createHandler()).
23
- * jsonTranslator only validates Content-Type and translates errors to JSON.
24
+ * Route dispatch happens via Express's registered route handlers (the per-route ExpressWrapper),
25
+ * NOT via this middleware.
24
26
  *
25
27
  * NEW: ExpressWrapper simplified - no longer handles JSON or headers
26
28
  * - JSON parsing/serialization moved to JsonFilter
@@ -35,19 +37,20 @@ export declare class WebpiecesMiddleware {
35
37
  /** The ONE wire<->context transfer, handed to every route's ExpressWrapper. Stateless. */
36
38
  private readonly headers;
37
39
  /**
38
- * Global error handler middleware - catches ALL unhandled errors.
39
- * Returns HTML 500 error page for any errors that escape the filter chain.
40
+ * Top-level error handler the last-ditch catch-all. MUST be mounted AFTER all routes (see
41
+ * {@link WebpiecesExpressRouter}). The 4-argument `(err, req, res, next)` signature is what
42
+ * tells express this is an error-handling middleware: express's router forwards ANY downstream
43
+ * failure to it — synchronous throws AND rejected async-handler promises alike (the router does
44
+ * `promise.then(null, err => next(err))`, and a `next(err)` with a truthy arg jumps straight to
45
+ * the first 4-arg middleware). This is why a `try { await next() } catch` wrapper is NOT needed
46
+ * (and would not work) — express `next()` is not promise-aware, so it never hands the parent the
47
+ * downstream promise; errors travel down this separate pipeline instead of bubbling back up.
40
48
  *
41
- * This is the outermost safety net - JsonTranslator catches JSON API errors,
42
- * this catches everything else.
49
+ * Returns an HTML 500 page. Api routes translate their own errors to JSON inside the filter
50
+ * chain (JsonFilter/ExpressWrapper), so this normally only fires for failures OUTSIDE a route
51
+ * (body parsing, unmatched paths, a bug in the wrapper itself).
43
52
  */
44
- globalErrorHandler(req: Request, res: Response, next: NextFunction): Promise<void>;
45
- /**
46
- * Logging middleware - logs request/response flow.
47
- * Demonstrates middleware execution order.
48
- * IMPORTANT: Must be async and await next() to properly chain with async middleware.
49
- */
50
- logNextLayer(req: Request, res: Response, next: NextFunction): Promise<void>;
53
+ errorHandler(err: unknown, req: Request, res: Response, next: NextFunction): void;
51
54
  /**
52
55
  * CORS middleware. DO NOT MOUNT UNCONDITIONALLY — {@link WebpiecesExpressRouter.bindAndStartExpress}
53
56
  * mounts it ONLY when {@link WebpiecesConfig.corsOrigins} is non-empty, and that is the point.
@@ -16,16 +16,18 @@ const corsLog = core_util_2.LogManager.getLogger('CORS');
16
16
  * WebpiecesMiddleware - Express middleware for WebPieces server.
17
17
  *
18
18
  * This class contains all Express middleware used by WebpiecesServer:
19
- * 1. globalErrorHandler - Outermost error handler, returns HTML 500 page
20
- * 2. logNextLayer - Request/response logging
21
- * 3. jsonTranslator - JSON Content-Type validation and error translation
19
+ * 1. errorHandler - Top-level 4-arg error middleware, returns HTML 500 page. Mounted AFTER the
20
+ * routes so express can forward downstream failures to it (express routes errors DOWN a
21
+ * separate pipeline, it does not bubble them back up through next()). Last-resort net only:
22
+ * api routes translate their own errors to JSON inside the filter chain (ExpressWrapper).
23
+ * 2. corsMiddleware - Opt-in CORS (mounted only when corsOrigins is non-empty).
22
24
  *
23
- * The middleware is injected into WebpiecesServerImpl and registered with Express
24
- * in the start() method.
25
+ * Per-request logging is intentionally NOT here the api filter chain's LogApiCall already logs
26
+ * every request/response with full context (requestId, method, path, body), so a plain express
27
+ * START/END line would only duplicate it with less information.
25
28
  *
26
- * IMPORTANT: jsonTranslator does NOT dispatch routes - route dispatch happens via
27
- * Express's registered route handlers (created by RouteBuilder.createHandler()).
28
- * jsonTranslator only validates Content-Type and translates errors to JSON.
29
+ * Route dispatch happens via Express's registered route handlers (the per-route ExpressWrapper),
30
+ * NOT via this middleware.
29
31
  *
30
32
  * NEW: ExpressWrapper simplified - no longer handles JSON or headers
31
33
  * - JSON parsing/serialization moved to JsonFilter
@@ -40,28 +42,29 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
40
42
  /** The ONE wire<->context transfer, handed to every route's ExpressWrapper. Stateless. */
41
43
  headers = new core_context_1.RequestContextHeaders();
42
44
  /**
43
- * Global error handler middleware - catches ALL unhandled errors.
44
- * Returns HTML 500 error page for any errors that escape the filter chain.
45
+ * Top-level error handler the last-ditch catch-all. MUST be mounted AFTER all routes (see
46
+ * {@link WebpiecesExpressRouter}). The 4-argument `(err, req, res, next)` signature is what
47
+ * tells express this is an error-handling middleware: express's router forwards ANY downstream
48
+ * failure to it — synchronous throws AND rejected async-handler promises alike (the router does
49
+ * `promise.then(null, err => next(err))`, and a `next(err)` with a truthy arg jumps straight to
50
+ * the first 4-arg middleware). This is why a `try { await next() } catch` wrapper is NOT needed
51
+ * (and would not work) — express `next()` is not promise-aware, so it never hands the parent the
52
+ * downstream promise; errors travel down this separate pipeline instead of bubbling back up.
45
53
  *
46
- * This is the outermost safety net - JsonTranslator catches JSON API errors,
47
- * this catches everything else.
54
+ * Returns an HTML 500 page. Api routes translate their own errors to JSON inside the filter
55
+ * chain (JsonFilter/ExpressWrapper), so this normally only fires for failures OUTSIDE a route
56
+ * (body parsing, unmatched paths, a bug in the wrapper itself).
48
57
  */
49
- async globalErrorHandler(req, res, next) {
50
- log.info(`🔴 [Layer 1: GlobalErrorHandler] Request START: ${req.method} ${req.path}`);
51
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Global error handler IS the top-level catch-all
52
- try {
53
- // await next() catches BOTH:
54
- // 1. Synchronous throws from next() itself
55
- // 2. Rejected promises from downstream async middleware
56
- await next();
57
- log.info(`🔴 [Layer 1: GlobalErrorHandler] Request END (success): ${req.method} ${req.path}`);
58
+ // webpieces-disable no-any-unknown -- a thrown/forwarded express error is genuinely unknown until narrowed
59
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- express needs the 4-arg (err,req,res,next) arity to recognize this as error-handling middleware
60
+ errorHandler(err, req, res, next) {
61
+ const error = (0, core_util_1.toError)(err);
62
+ log.error(`Unhandled error: ${req.method} ${req.path}`, error);
63
+ if (res.headersSent) {
64
+ return;
58
65
  }
59
- catch (err) {
60
- const error = (0, core_util_1.toError)(err);
61
- log.error('🔴 [Layer 1: GlobalErrorHandler] Caught unhandled error:', error);
62
- if (!res.headersSent) {
63
- // Return HTML error page (not JSON - JsonTranslator handles JSON errors)
64
- res.status(500).send(`
66
+ // Return HTML error page (not JSON - api routes translate JSON errors in their filter chain)
67
+ res.status(500).send(`
65
68
  <!DOCTYPE html>
66
69
  <html>
67
70
  <head><title>Server Error</title></head>
@@ -72,19 +75,6 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
72
75
  </body>
73
76
  </html>
74
77
  `);
75
- }
76
- log.info(`🔴 [Layer 1: GlobalErrorHandler] Request END (error): ${req.method} ${req.path}`);
77
- }
78
- }
79
- /**
80
- * Logging middleware - logs request/response flow.
81
- * Demonstrates middleware execution order.
82
- * IMPORTANT: Must be async and await next() to properly chain with async middleware.
83
- */
84
- async logNextLayer(req, res, next) {
85
- log.info(`🟡 [Layer 2: LogNextLayer] Before next() - ${req.method} ${req.path}`);
86
- await next();
87
- log.info(`🟡 [Layer 2: LogNextLayer] After next() - ${req.method} ${req.path}`);
88
78
  }
89
79
  /**
90
80
  * CORS middleware. DO NOT MOUNT UNCONDITIONALLY — {@link WebpiecesExpressRouter.bindAndStartExpress}
@@ -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 { 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"]}
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. errorHandler - Top-level 4-arg error middleware, returns HTML 500 page. Mounted AFTER the\n * routes so express can forward downstream failures to it (express routes errors DOWN a\n * separate pipeline, it does not bubble them back up through next()). Last-resort net only:\n * api routes translate their own errors to JSON inside the filter chain (ExpressWrapper).\n * 2. corsMiddleware - Opt-in CORS (mounted only when corsOrigins is non-empty).\n *\n * Per-request logging is intentionally NOT here — the api filter chain's LogApiCall already logs\n * every request/response with full context (requestId, method, path, body), so a plain express\n * START/END line would only duplicate it with less information.\n *\n * Route dispatch happens via Express's registered route handlers (the per-route ExpressWrapper),\n * NOT via this middleware.\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 * Top-level error handler — the last-ditch catch-all. MUST be mounted AFTER all routes (see\n * {@link WebpiecesExpressRouter}). The 4-argument `(err, req, res, next)` signature is what\n * tells express this is an error-handling middleware: express's router forwards ANY downstream\n * failure to it — synchronous throws AND rejected async-handler promises alike (the router does\n * `promise.then(null, err => next(err))`, and a `next(err)` with a truthy arg jumps straight to\n * the first 4-arg middleware). This is why a `try { await next() } catch` wrapper is NOT needed\n * (and would not work) — express `next()` is not promise-aware, so it never hands the parent the\n * downstream promise; errors travel down this separate pipeline instead of bubbling back up.\n *\n * Returns an HTML 500 page. Api routes translate their own errors to JSON inside the filter\n * chain (JsonFilter/ExpressWrapper), so this normally only fires for failures OUTSIDE a route\n * (body parsing, unmatched paths, a bug in the wrapper itself).\n */\n // webpieces-disable no-any-unknown -- a thrown/forwarded express error is genuinely unknown until narrowed\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- express needs the 4-arg (err,req,res,next) arity to recognize this as error-handling middleware\n errorHandler(err: unknown, req: Request, res: Response, next: NextFunction): void {\n const error = toError(err);\n log.error(`Unhandled error: ${req.method} ${req.path}`, error);\n if (res.headersSent) {\n return;\n }\n // Return HTML error page (not JSON - api routes translate JSON errors in their filter chain)\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\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"]}
@@ -6,6 +6,7 @@ const http_routing_1 = require("@webpieces/http-routing");
6
6
  const http_routing_2 = require("@webpieces/http-routing");
7
7
  const core_util_1 = require("@webpieces/core-util");
8
8
  const core_util_2 = require("@webpieces/core-util");
9
+ const core_context_1 = require("@webpieces/core-context");
9
10
  /**
10
11
  * LogApiFilter - Structured API logging for all requests/responses.
11
12
  * Priority: 1800 (after ContextFilter at 2000, before custom filters)
@@ -36,6 +37,17 @@ let LogApiFilter = class LogApiFilter extends http_routing_2.Filter {
36
37
  // the backend. apiClass is the CONTRACT name (routeMeta.apiName, e.g. 'SaveApi') so a server log
37
38
  // line MATCHES the client's for the same call; controllerName keeps the impl (e.g. 'SaveController').
38
39
  const rm = meta.routeMeta;
40
+ // Stamp the routed endpoint's IMPLEMENTATION identity onto the request context so EVERY log line
41
+ // of this request (not just the api req/resp lines) carries the concrete controller class +
42
+ // handler method name — what you actually grep for, and more useful than the raw requestPath. GCP
43
+ // gets them as separate jsonPayload.controller / jsonPayload.method; the local console formatters
44
+ // render them together as a compact [Controller.method] bracket. They clear with the request scope.
45
+ if (rm.controllerClassName) {
46
+ core_context_1.RequestContext.putHeader(core_util_1.WebpiecesCoreHeaders.CONTROLLER, rm.controllerClassName);
47
+ }
48
+ if (rm.methodName) {
49
+ core_context_1.RequestContext.putHeader(core_util_1.WebpiecesCoreHeaders.METHOD, rm.methodName);
50
+ }
39
51
  const info = new core_util_2.ApiMethodInfo('server', rm.apiName ?? rm.controllerClassName ?? 'Unknown', rm.methodName, rm.controllerClassName);
40
52
  const response = await core_util_2.LogApiCall.execute(info, meta.requestDto, method);
41
53
  return new http_routing_2.WpResponse(response);
@@ -1 +1 @@
1
- {"version":3,"file":"LogApiFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/filters/LogApiFilter.ts"],"names":[],"mappings":";;;;AAAA,0DAA8E;AAC9E,0DAAsE;AACtE,oDAAkD;AAClD,oDAAiE;AAEjE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;AAG1C,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,qBAAuC;IAErE,KAAK,CAAC,MAAM,CACR,IAAgB,EAChB,UAAoD;QAEpD,+DAA+D;QAC/D,MAAM,MAAM,GAAG,KAAK,IAAsB,EAAE;YACxC,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACjD,OAAO,UAAU,CAAC,QAAQ,CAAC;QAC/B,CAAC,CAAC;QAEF,+FAA+F;QAC/F,+FAA+F;QAC/F,+FAA+F;QAC/F,iGAAiG;QACjG,sGAAsG;QACtG,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;QAC1B,MAAM,IAAI,GAAG,IAAI,yBAAa,CAC1B,QAAQ,EACR,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,mBAAmB,IAAI,SAAS,EACjD,EAAE,CAAC,UAAU,EACb,EAAE,CAAC,mBAAmB,CACzB,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,sBAAU,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACzE,OAAO,IAAI,yBAAU,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;CACJ,CAAA;AA3BY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,wCAAyB,GAAE;GACf,YAAY,CA2BxB","sourcesContent":["import {provideFrameworkSingleton, MethodMeta} from '@webpieces/http-routing';\nimport { Filter, WpResponse, Service } from '@webpieces/http-routing';\nimport { LogManager } from '@webpieces/core-util';\nimport { LogApiCall, ApiMethodInfo } from '@webpieces/core-util';\n\n/**\n * LogApiFilter - Structured API logging for all requests/responses.\n * Priority: 1800 (after ContextFilter at 2000, before custom filters)\n *\n * Logging patterns (via LogApiCall):\n * - [API-server-req] Class.method /url request={...}\n * - [API-server-resp-SUCCESS] Class.method response={...}\n * - [API-server-resp-FAIL] Class.method error=... (server errors: 500, 502, 504)\n * - [API-server-resp-OTHER] Class.method errorType=... (user errors: 400, 401, 403, 404, 266)\n *\n * Headers are read from RequestContext (NOT from meta.requestHeaders which is undefined\n * after ContextFilter runs at priority 2000).\n *\n * User errors (HttpBadRequestError, etc.) are logged as OTHER, not FAIL,\n * because they are expected behavior from the server's perspective.\n */\nconst log = LogManager.getLogger('LogApiFilter');\n\n@provideFrameworkSingleton()\nexport class LogApiFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n\n async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n // Wrap nextFilter.invoke in a method that returns the response\n const method = async (): Promise<unknown> => {\n const wpResponse = await nextFilter.invoke(meta);\n return wpResponse.response;\n };\n\n // LogApiCall is a singleton (use it directly, no `new`). It logs the text lines AND stamps the\n // structured `api={method:{side:'server',...},...}` tag into RequestContext, so every log line\n // during the request carries jsonPayload.api. Correlation fields (requestId, ...) are added by\n // the backend. apiClass is the CONTRACT name (routeMeta.apiName, e.g. 'SaveApi') so a server log\n // line MATCHES the client's for the same call; controllerName keeps the impl (e.g. 'SaveController').\n const rm = meta.routeMeta;\n const info = new ApiMethodInfo(\n 'server',\n rm.apiName ?? rm.controllerClassName ?? 'Unknown',\n rm.methodName,\n rm.controllerClassName,\n );\n const response = await LogApiCall.execute(info, meta.requestDto, method);\n return new WpResponse(response);\n }\n}\n"]}
1
+ {"version":3,"file":"LogApiFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/filters/LogApiFilter.ts"],"names":[],"mappings":";;;;AAAA,0DAA8E;AAC9E,0DAAsE;AACtE,oDAAwE;AACxE,oDAAiE;AACjE,0DAAyD;AAEzD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;AAG1C,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,qBAAuC;IAErE,KAAK,CAAC,MAAM,CACR,IAAgB,EAChB,UAAoD;QAEpD,+DAA+D;QAC/D,MAAM,MAAM,GAAG,KAAK,IAAsB,EAAE;YACxC,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACjD,OAAO,UAAU,CAAC,QAAQ,CAAC;QAC/B,CAAC,CAAC;QAEF,+FAA+F;QAC/F,+FAA+F;QAC/F,+FAA+F;QAC/F,iGAAiG;QACjG,sGAAsG;QACtG,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;QAE1B,iGAAiG;QACjG,4FAA4F;QAC5F,kGAAkG;QAClG,kGAAkG;QAClG,oGAAoG;QACpG,IAAI,EAAE,CAAC,mBAAmB,EAAE,CAAC;YACzB,6BAAc,CAAC,SAAS,CAAC,gCAAoB,CAAC,UAAU,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,EAAE,CAAC,UAAU,EAAE,CAAC;YAChB,6BAAc,CAAC,SAAS,CAAC,gCAAoB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC;QACzE,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,yBAAa,CAC1B,QAAQ,EACR,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,mBAAmB,IAAI,SAAS,EACjD,EAAE,CAAC,UAAU,EACb,EAAE,CAAC,mBAAmB,CACzB,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,sBAAU,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACzE,OAAO,IAAI,yBAAU,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;CACJ,CAAA;AAxCY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,wCAAyB,GAAE;GACf,YAAY,CAwCxB","sourcesContent":["import {provideFrameworkSingleton, MethodMeta} from '@webpieces/http-routing';\nimport { Filter, WpResponse, Service } from '@webpieces/http-routing';\nimport { LogManager, WebpiecesCoreHeaders } from '@webpieces/core-util';\nimport { LogApiCall, ApiMethodInfo } from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\n\n/**\n * LogApiFilter - Structured API logging for all requests/responses.\n * Priority: 1800 (after ContextFilter at 2000, before custom filters)\n *\n * Logging patterns (via LogApiCall):\n * - [API-server-req] Class.method /url request={...}\n * - [API-server-resp-SUCCESS] Class.method response={...}\n * - [API-server-resp-FAIL] Class.method error=... (server errors: 500, 502, 504)\n * - [API-server-resp-OTHER] Class.method errorType=... (user errors: 400, 401, 403, 404, 266)\n *\n * Headers are read from RequestContext (NOT from meta.requestHeaders which is undefined\n * after ContextFilter runs at priority 2000).\n *\n * User errors (HttpBadRequestError, etc.) are logged as OTHER, not FAIL,\n * because they are expected behavior from the server's perspective.\n */\nconst log = LogManager.getLogger('LogApiFilter');\n\n@provideFrameworkSingleton()\nexport class LogApiFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n\n async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n // Wrap nextFilter.invoke in a method that returns the response\n const method = async (): Promise<unknown> => {\n const wpResponse = await nextFilter.invoke(meta);\n return wpResponse.response;\n };\n\n // LogApiCall is a singleton (use it directly, no `new`). It logs the text lines AND stamps the\n // structured `api={method:{side:'server',...},...}` tag into RequestContext, so every log line\n // during the request carries jsonPayload.api. Correlation fields (requestId, ...) are added by\n // the backend. apiClass is the CONTRACT name (routeMeta.apiName, e.g. 'SaveApi') so a server log\n // line MATCHES the client's for the same call; controllerName keeps the impl (e.g. 'SaveController').\n const rm = meta.routeMeta;\n\n // Stamp the routed endpoint's IMPLEMENTATION identity onto the request context so EVERY log line\n // of this request (not just the api req/resp lines) carries the concrete controller class +\n // handler method name — what you actually grep for, and more useful than the raw requestPath. GCP\n // gets them as separate jsonPayload.controller / jsonPayload.method; the local console formatters\n // render them together as a compact [Controller.method] bracket. They clear with the request scope.\n if (rm.controllerClassName) {\n RequestContext.putHeader(WebpiecesCoreHeaders.CONTROLLER, rm.controllerClassName);\n }\n if (rm.methodName) {\n RequestContext.putHeader(WebpiecesCoreHeaders.METHOD, rm.methodName);\n }\n\n const info = new ApiMethodInfo(\n 'server',\n rm.apiName ?? rm.controllerClassName ?? 'Unknown',\n rm.methodName,\n rm.controllerClassName,\n );\n const response = await LogApiCall.execute(info, meta.requestDto, method);\n return new WpResponse(response);\n }\n}\n"]}