@webpieces/http-server 0.4.402 → 0.4.403
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.
|
|
3
|
+
"version": "0.4.403",
|
|
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.403",
|
|
26
|
+
"@webpieces/core-util": "0.4.403",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.403",
|
|
28
|
+
"@webpieces/http-routing": "0.4.403",
|
|
29
29
|
"cors": "2.8.5",
|
|
30
30
|
"express": "5.1.0",
|
|
31
31
|
"inversify": "7.10.4"
|
|
@@ -44,6 +44,12 @@ export declare class WebpiecesExpressRouter {
|
|
|
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 +
|
|
@@ -76,10 +76,33 @@ class WebpiecesExpressRouter {
|
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
log.info(`Listening on http://localhost:${port}`);
|
|
79
|
+
this.logStartupBanner(port);
|
|
79
80
|
resolve(server);
|
|
80
81
|
});
|
|
81
82
|
});
|
|
82
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The "Svr Ready!!" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and
|
|
86
|
+
* every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied
|
|
87
|
+
* verbatim from the trytami service so a familiar splash marks "the server is up and reachable".
|
|
88
|
+
*/
|
|
89
|
+
logStartupBanner(port) {
|
|
90
|
+
if (process.env['K_SERVICE']) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
log.info(`
|
|
94
|
+
___ _____ _
|
|
95
|
+
/ _| | _ \\ | |
|
|
96
|
+
\\ \`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _
|
|
97
|
+
\`--. \\/ _ \\ '_\\ \\ / / _ \\ '_| | // _ \\/ _\` |/ _\` | | | |
|
|
98
|
+
/\\_/ / _/ | \\ V / _/ | | |\\ \\ _/ (_| | (_| | |_| |
|
|
99
|
+
\\___/ \\_|_| \\_/ \\_|_| \\_| \\_\\_|\\_,_|\\_,_|\\_, |
|
|
100
|
+
_/ |
|
|
101
|
+
|_/
|
|
102
|
+
|
|
103
|
+
Svr Ready!! port=${port}
|
|
104
|
+
`);
|
|
105
|
+
}
|
|
83
106
|
/**
|
|
84
107
|
* Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;
|
|
85
108
|
* 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 (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 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"]}
|
|
@@ -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,
|
|
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"]}
|