@3sixty/api-core-server 0.20.0-next.0 → 0.20.0-next.1

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.
Files changed (31) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +20 -2
  3. package/dist/es/baseServer.js +666 -0
  4. package/dist/es/baseServer.js.map +1 -0
  5. package/dist/es/index.js +8 -0
  6. package/dist/es/index.js.map +1 -0
  7. package/dist/es/models/IBaseServerConstructorOptions.js +2 -0
  8. package/dist/es/models/IBaseServerConstructorOptions.js.map +1 -0
  9. package/dist/es/models/IRestProcessorChains.js +2 -0
  10. package/dist/es/models/IRestProcessorChains.js.map +1 -0
  11. package/dist/es/models/IServerCorsOptions.js +2 -0
  12. package/dist/es/models/IServerCorsOptions.js.map +1 -0
  13. package/dist/es/models/ISocketProcessorChains.js +2 -0
  14. package/dist/es/models/ISocketProcessorChains.js.map +1 -0
  15. package/dist/types/baseServer.d.ts +232 -0
  16. package/dist/types/index.d.ts +5 -0
  17. package/dist/types/models/IBaseServerConstructorOptions.d.ts +19 -0
  18. package/dist/types/models/IRestProcessorChains.d.ts +18 -0
  19. package/dist/types/models/IServerCorsOptions.d.ts +26 -0
  20. package/dist/types/models/ISocketProcessorChains.d.ts +26 -0
  21. package/docs/changelog.md +16 -0
  22. package/docs/examples.md +78 -0
  23. package/docs/reference/classes/BaseServer.md +730 -0
  24. package/docs/reference/index.md +12 -0
  25. package/docs/reference/interfaces/IBaseServerConstructorOptions.md +33 -0
  26. package/docs/reference/interfaces/IRestProcessorChains.md +171 -0
  27. package/docs/reference/interfaces/IServerCorsOptions.md +43 -0
  28. package/docs/reference/interfaces/ISocketProcessorChains.md +245 -0
  29. package/locales/.validate-ignore +3 -0
  30. package/locales/en.json +33 -0
  31. package/package.json +54 -6
@@ -0,0 +1,666 @@
1
+ // Copyright 2026 IOTA Stiftung.
2
+ // Copyright 2026 3Sixty Trade Ltd.
3
+ // SPDX-License-Identifier: Apache-2.0.
4
+ import { request as httpRequest, validateHeaderName, validateHeaderValue } from "node:http";
5
+ import { request as httpsRequest } from "node:https";
6
+ import { HealthCategory, HealthStatus, HttpBodyLimit, HttpContextIdKeys, HttpErrorHelper } from "@3sixty/api-models";
7
+ import { JsonLdMimeTypeProcessor } from "@3sixty/api-processors";
8
+ import { ContextIdStore } from "@3sixty/context";
9
+ import { BaseError, ComponentFactory, GeneralError, Is, RandomHelper, Url } from "@3sixty/core";
10
+ import { HeaderHelper, HeaderTypes, HttpMethod, HttpStatusCode, MimeTypes } from "@3sixty/web";
11
+ /**
12
+ * Base class for web servers, containing the transport agnostic route processing.
13
+ * Derived classes supply the transport specific behaviour through the abstract server methods.
14
+ */
15
+ export class BaseServer {
16
+ /**
17
+ * Runtime name for the class.
18
+ */
19
+ static CLASS_NAME = "BaseServer";
20
+ /**
21
+ * Default port for running the server.
22
+ * @internal
23
+ */
24
+ static _DEFAULT_PORT = 3000;
25
+ /**
26
+ * Default host for running the server.
27
+ * @internal
28
+ */
29
+ static _DEFAULT_HOST = "localhost";
30
+ /**
31
+ * Default named body size limits for routes.
32
+ * @internal
33
+ */
34
+ static _DEFAULT_BODY_LIMITS = {
35
+ [HttpBodyLimit.Default]: 1048576,
36
+ [HttpBodyLimit.Large]: 26214400
37
+ };
38
+ /**
39
+ * The logging component type.
40
+ */
41
+ _loggingComponentType;
42
+ /**
43
+ * The logging component.
44
+ */
45
+ _logging;
46
+ /**
47
+ * The options for the server.
48
+ */
49
+ _options;
50
+ /**
51
+ * Whether the server has been started.
52
+ */
53
+ _started;
54
+ /**
55
+ * The mime type processors.
56
+ */
57
+ _mimeTypeProcessors;
58
+ /**
59
+ * Include the stack with errors.
60
+ */
61
+ _includeErrorStack;
62
+ /**
63
+ * The public origin of the server, used for constructing the request URL and for CORS.
64
+ */
65
+ _publicOrigin;
66
+ /**
67
+ * The local origin of the server, used for constructing the request URL and for CORS.
68
+ */
69
+ _localOrigin;
70
+ /**
71
+ * The REST processor chains resolved when the server is built.
72
+ */
73
+ _restChains;
74
+ /**
75
+ * The socket processor chains resolved when the server is built.
76
+ */
77
+ _socketChains;
78
+ /**
79
+ * Create a new instance of BaseServer.
80
+ * @param options The options for the server.
81
+ */
82
+ constructor(options) {
83
+ this._loggingComponentType = options?.loggingComponentType;
84
+ this._logging = ComponentFactory.getIfExists(options?.loggingComponentType);
85
+ this._started = false;
86
+ this._restChains = { pre: [], process: [], post: [] };
87
+ this._socketChains = { connected: [], disconnected: [], pre: [], process: [], post: [] };
88
+ this._mimeTypeProcessors = options?.mimeTypeProcessors ?? [];
89
+ this._includeErrorStack = options?.includeErrorStack ?? false;
90
+ const hasJsonLd = this._mimeTypeProcessors.some(processor => processor.getTypes().includes(MimeTypes.JsonLd));
91
+ if (!hasJsonLd) {
92
+ this._mimeTypeProcessors.push(new JsonLdMimeTypeProcessor());
93
+ }
94
+ }
95
+ /**
96
+ * Start the server.
97
+ * @returns A promise that resolves when the server is listening for connections.
98
+ */
99
+ async start() {
100
+ const host = this._options?.host ?? BaseServer._DEFAULT_HOST;
101
+ const port = this._options?.port ?? BaseServer._DEFAULT_PORT;
102
+ await this._logging?.log({
103
+ level: "info",
104
+ ts: Date.now(),
105
+ source: BaseServer.CLASS_NAME,
106
+ message: "starting",
107
+ data: {
108
+ host,
109
+ port
110
+ }
111
+ });
112
+ if (!this._started) {
113
+ try {
114
+ await this.serverListen(host, port);
115
+ const addresses = this.serverAddresses();
116
+ const protocol = this.serverIsSecure() ? "https://" : "http://";
117
+ const normalizeAnyHost = (address) => {
118
+ if (address === "0.0.0.0") {
119
+ return "127.0.0.1";
120
+ }
121
+ if (address === "::") {
122
+ return "::1";
123
+ }
124
+ return address;
125
+ };
126
+ const formatAddress = (address, family, forceProtocol) => {
127
+ const displayAddress = normalizeAnyHost(address);
128
+ const isIPv6 = family === "IPv6" || (Is.stringValue(displayAddress) && displayAddress.includes(":"));
129
+ return `${forceProtocol ?? protocol}${isIPv6 ? "[" : ""}${displayAddress}${isIPv6 ? "]" : ""}`;
130
+ };
131
+ const startupAddresses = addresses.map(a => {
132
+ const formatted = formatAddress(a.address, a.family, protocol);
133
+ return `${formatted}:${a.port}`;
134
+ });
135
+ for (const origin of [this._localOrigin, this._publicOrigin]) {
136
+ if (Is.stringValue(origin)) {
137
+ const originUrl = Url.tryParseExact(origin);
138
+ if (!Is.empty(originUrl)) {
139
+ const originParts = originUrl.parts();
140
+ const formatted = formatAddress(originParts.host, undefined, `${originParts.schema}://`);
141
+ startupAddresses.push(`${formatted}${Is.integer(originParts.port) ? `:${originParts.port}` : ""}`);
142
+ }
143
+ else {
144
+ startupAddresses.push(origin);
145
+ }
146
+ }
147
+ }
148
+ const distinctStartupAddresses = [...new Set(startupAddresses)];
149
+ await this._logging?.log({
150
+ level: "info",
151
+ ts: Date.now(),
152
+ source: BaseServer.CLASS_NAME,
153
+ message: "started",
154
+ data: {
155
+ addresses: distinctStartupAddresses.join(", ")
156
+ }
157
+ });
158
+ this._started = true;
159
+ }
160
+ catch (err) {
161
+ await this._logging?.log({
162
+ level: "error",
163
+ ts: Date.now(),
164
+ source: BaseServer.CLASS_NAME,
165
+ message: "startFailed",
166
+ error: BaseError.fromError(err)
167
+ });
168
+ }
169
+ }
170
+ }
171
+ /**
172
+ * Stop the server.
173
+ * @returns A promise that resolves when the server has shut down all connections.
174
+ */
175
+ async stop() {
176
+ if (this._started) {
177
+ this._started = false;
178
+ await this.serverClose();
179
+ await this._logging?.log({
180
+ level: "info",
181
+ ts: Date.now(),
182
+ source: BaseServer.CLASS_NAME,
183
+ message: "stopped"
184
+ });
185
+ }
186
+ }
187
+ /**
188
+ * Returns the health status of the component.
189
+ * @returns The health status of the component, can return multiple entries for elements within the component.
190
+ */
191
+ async health() {
192
+ let healthCheck;
193
+ if (this.serverIsListening()) {
194
+ healthCheck = {
195
+ source: BaseServer.CLASS_NAME,
196
+ status: HealthStatus.Ok,
197
+ category: HealthCategory.Connectivity,
198
+ description: "healthConnectivityDescription",
199
+ message: "reachable"
200
+ };
201
+ }
202
+ else {
203
+ healthCheck = {
204
+ source: BaseServer.CLASS_NAME,
205
+ status: HealthStatus.Error,
206
+ category: HealthCategory.Connectivity,
207
+ description: "healthConnectivityDescription",
208
+ message: "unreachable"
209
+ };
210
+ }
211
+ return [healthCheck];
212
+ }
213
+ /**
214
+ * Verify the root endpoint is reachable and returns a body by making a real HTTP request.
215
+ * Skipped when GET / is not registered on this server instance.
216
+ * @param callback The callback to invoke when a deferred health result is ready.
217
+ * @returns The application health status of the component.
218
+ */
219
+ async healthApplication(callback) {
220
+ if (!this.serverHasRootRoute()) {
221
+ return [];
222
+ }
223
+ if (!Is.stringValue(this._localOrigin)) {
224
+ return [
225
+ {
226
+ source: BaseServer.CLASS_NAME,
227
+ status: HealthStatus.Error,
228
+ category: HealthCategory.Application,
229
+ description: "healthApplicationDescription",
230
+ message: "serverNotBuilt"
231
+ }
232
+ ];
233
+ }
234
+ try {
235
+ const response = await this.requestLocalRoot(`${this._localOrigin}/`);
236
+ if (response.ok && Is.stringValue(response.body)) {
237
+ return [
238
+ {
239
+ source: BaseServer.CLASS_NAME,
240
+ status: HealthStatus.Ok,
241
+ category: HealthCategory.Application,
242
+ description: "healthApplicationDescription",
243
+ message: "rootEndpointReachable"
244
+ }
245
+ ];
246
+ }
247
+ return [
248
+ {
249
+ source: BaseServer.CLASS_NAME,
250
+ status: HealthStatus.Error,
251
+ category: HealthCategory.Application,
252
+ description: "healthApplicationDescription",
253
+ message: "rootEndpointError"
254
+ }
255
+ ];
256
+ }
257
+ catch (error) {
258
+ return [
259
+ {
260
+ source: BaseServer.CLASS_NAME,
261
+ status: HealthStatus.Error,
262
+ category: HealthCategory.Application,
263
+ description: "healthApplicationDescription",
264
+ error: BaseError.fromError(error)
265
+ }
266
+ ];
267
+ }
268
+ }
269
+ /**
270
+ * Request the root endpoint of this server over the loopback address.
271
+ * @param url The url of the root endpoint.
272
+ * @returns Whether the response was successful and its body.
273
+ */
274
+ async requestLocalRoot(url) {
275
+ const isSecure = url.startsWith("https:");
276
+ return new Promise((resolve, reject) => {
277
+ const callback = (response) => {
278
+ const chunks = [];
279
+ response.on("data", (chunk) => chunks.push(chunk));
280
+ response.on("error", reject);
281
+ response.on("end", () => {
282
+ const statusCode = response.statusCode ?? 0;
283
+ resolve({
284
+ ok: statusCode >= HttpStatusCode.ok && statusCode < HttpStatusCode.multipleChoices,
285
+ body: Buffer.concat(chunks).toString("utf8")
286
+ });
287
+ });
288
+ };
289
+ // The certificate is issued for the public host name rather than the loopback
290
+ // address, and this only checks the server can reach its own listener, so it
291
+ // is not verified
292
+ const request = isSecure
293
+ ? httpsRequest(url, { rejectUnauthorized: false }, callback)
294
+ : httpRequest(url, callback);
295
+ request.on("error", reject);
296
+ request.end();
297
+ });
298
+ }
299
+ /**
300
+ * Perform the transport agnostic part of building the server, validating the routes and
301
+ * processors, resolving the origins and resolving the processor chains.
302
+ * @param restRouteProcessors The processors for incoming requests over REST.
303
+ * @param restRoutes The REST routes.
304
+ * @param socketRouteProcessors The processors for incoming requests over Sockets.
305
+ * @param socketRoutes The socket routes.
306
+ * @param options Options for building the server.
307
+ * @returns A promise that resolves when the shared build steps are complete.
308
+ */
309
+ async prepareBuild(restRouteProcessors, restRoutes, socketRouteProcessors, socketRoutes, options) {
310
+ if (Is.arrayValue(restRoutes) && !Is.arrayValue(restRouteProcessors)) {
311
+ throw new GeneralError(BaseServer.CLASS_NAME, "noRestProcessors");
312
+ }
313
+ if (Is.arrayValue(socketRoutes) && !Is.arrayValue(socketRouteProcessors)) {
314
+ throw new GeneralError(BaseServer.CLASS_NAME, "noSocketProcessors");
315
+ }
316
+ await this._logging?.log({
317
+ level: "info",
318
+ ts: Date.now(),
319
+ source: BaseServer.CLASS_NAME,
320
+ message: "building"
321
+ });
322
+ let localHost = options?.host ?? "127.0.0.1";
323
+ if (localHost === "0.0.0.0") {
324
+ localHost = "127.0.0.1";
325
+ }
326
+ this._localOrigin = `${this.serverIsSecure() ? "https" : "http"}://${localHost}:${options?.port ?? 3000}`;
327
+ if (Is.stringValue(options?.publicOrigin)) {
328
+ const publicUrl = Url.tryParseExact(options.publicOrigin);
329
+ if (!Is.empty(publicUrl)) {
330
+ const urlParts = publicUrl.parts();
331
+ this._publicOrigin = `${urlParts.schema}://${urlParts.host}${Is.integer(urlParts.port) ? `:${urlParts.port}` : ""}`;
332
+ }
333
+ else {
334
+ throw new GeneralError(BaseServer.CLASS_NAME, "invalidPublicOrigin", {
335
+ publicOrigin: options.publicOrigin
336
+ });
337
+ }
338
+ }
339
+ this._options = options;
340
+ this.buildProcessorChains(restRouteProcessors, socketRouteProcessors);
341
+ }
342
+ /**
343
+ * Resolve the route processors in to per phase chains, so that the lookups and bindings
344
+ * are performed once when the server is built instead of on every request.
345
+ * @param restRouteProcessors The processors for the incoming REST requests.
346
+ * @param socketRouteProcessors The processors for the incoming socket requests.
347
+ */
348
+ buildProcessorChains(restRouteProcessors, socketRouteProcessors) {
349
+ const restChains = { pre: [], process: [], post: [] };
350
+ for (const routeProcessor of restRouteProcessors ?? []) {
351
+ const pre = routeProcessor.pre?.bind(routeProcessor);
352
+ if (Is.function(pre)) {
353
+ restChains.pre.push(pre);
354
+ }
355
+ const process = routeProcessor.process?.bind(routeProcessor);
356
+ if (Is.function(process)) {
357
+ restChains.process.push(process);
358
+ }
359
+ const post = routeProcessor.post?.bind(routeProcessor);
360
+ if (Is.function(post)) {
361
+ restChains.post.push(post);
362
+ }
363
+ }
364
+ const socketChains = {
365
+ connected: [],
366
+ disconnected: [],
367
+ pre: [],
368
+ process: [],
369
+ post: []
370
+ };
371
+ for (const routeProcessor of socketRouteProcessors ?? []) {
372
+ const connected = routeProcessor.connected?.bind(routeProcessor);
373
+ if (Is.function(connected)) {
374
+ socketChains.connected.push(connected);
375
+ }
376
+ const disconnected = routeProcessor.disconnected?.bind(routeProcessor);
377
+ if (Is.function(disconnected)) {
378
+ socketChains.disconnected.push(disconnected);
379
+ }
380
+ const pre = routeProcessor.pre?.bind(routeProcessor);
381
+ if (Is.function(pre)) {
382
+ socketChains.pre.push(pre);
383
+ }
384
+ const process = routeProcessor.process?.bind(routeProcessor);
385
+ if (Is.function(process)) {
386
+ socketChains.process.push(process);
387
+ }
388
+ const post = routeProcessor.post?.bind(routeProcessor);
389
+ if (Is.function(post)) {
390
+ socketChains.post.push(post);
391
+ }
392
+ }
393
+ this._restChains = restChains;
394
+ this._socketChains = socketChains;
395
+ }
396
+ /**
397
+ * Merge the configured body limits over the built-in ones and validate them.
398
+ * @returns The named body limits in bytes.
399
+ * @throws GeneralError If a limit is not a positive integer.
400
+ */
401
+ resolveBodyLimits() {
402
+ const bodyLimits = {
403
+ ...BaseServer._DEFAULT_BODY_LIMITS,
404
+ ...this._options?.bodyLimits
405
+ };
406
+ for (const name of Object.keys(bodyLimits)) {
407
+ const bodyLimit = bodyLimits[name];
408
+ if (!Is.integer(bodyLimit) || bodyLimit <= 0) {
409
+ throw new GeneralError(BaseServer.CLASS_NAME, "invalidBodyLimit", {
410
+ bodyLimit: name,
411
+ value: bodyLimit
412
+ });
413
+ }
414
+ }
415
+ return bodyLimits;
416
+ }
417
+ /**
418
+ * Resolve the CORS options from the web server options, merging in the defaults.
419
+ * @param options The web server options.
420
+ * @returns The resolved CORS options.
421
+ */
422
+ resolveCorsOptions(options) {
423
+ let origins = ["*"];
424
+ if (Is.arrayValue(options?.corsOrigins)) {
425
+ origins = options?.corsOrigins;
426
+ }
427
+ else if (Is.stringValue(options?.corsOrigins)) {
428
+ origins = [options?.corsOrigins];
429
+ }
430
+ const hasWildcardOrigin = origins.includes("*");
431
+ const methods = options?.methods ?? [
432
+ HttpMethod.GET,
433
+ HttpMethod.PUT,
434
+ HttpMethod.POST,
435
+ HttpMethod.PATCH,
436
+ HttpMethod.DELETE,
437
+ HttpMethod.OPTIONS
438
+ ];
439
+ const allowedHeaders = [
440
+ HeaderTypes.AccessControlAllowOrigin,
441
+ HeaderTypes.ContentEncoding,
442
+ HeaderTypes.AcceptEncoding,
443
+ HeaderTypes.ContentType,
444
+ HeaderTypes.Authorization,
445
+ HeaderTypes.Accept
446
+ ];
447
+ const exposedHeaders = [HeaderTypes.ContentDisposition, HeaderTypes.Location];
448
+ if (Is.arrayValue(options?.allowedHeaders)) {
449
+ allowedHeaders.push(...options.allowedHeaders);
450
+ }
451
+ if (Is.arrayValue(options?.exposedHeaders)) {
452
+ exposedHeaders.push(...options.exposedHeaders);
453
+ }
454
+ return { origins, hasWildcardOrigin, methods, allowedHeaders, exposedHeaders };
455
+ }
456
+ /**
457
+ * Build the server request from the transport values, the path params and query must already
458
+ * have been decoded by the transport, as decoding them again corrupts values containing %.
459
+ * @param method The request method.
460
+ * @param url The full request url, including the origin.
461
+ * @param body The request body.
462
+ * @param query The request query params.
463
+ * @param pathParams The request path params.
464
+ * @param headers The request headers.
465
+ * @returns The server request.
466
+ */
467
+ buildServerRequest(method, url, body, query, pathParams, headers) {
468
+ return {
469
+ method,
470
+ url,
471
+ body,
472
+ query,
473
+ pathParams,
474
+ headers
475
+ };
476
+ }
477
+ /**
478
+ * Validate the processed response before it is sent, so that every transport fails an invalid
479
+ * response in the same way rather than with its own transport specific error.
480
+ * @param httpResponse The processed response.
481
+ * @throws GeneralError If the status code or a header is invalid.
482
+ */
483
+ validateResponse(httpResponse) {
484
+ const statusCode = httpResponse.statusCode;
485
+ if (!Is.empty(statusCode) &&
486
+ (!Is.integer(statusCode) || statusCode < 100 || statusCode > 599)) {
487
+ throw new GeneralError(BaseServer.CLASS_NAME, "invalidStatusCode", { statusCode });
488
+ }
489
+ if (!Is.empty(httpResponse.headers)) {
490
+ for (const header of Object.keys(httpResponse.headers)) {
491
+ const value = httpResponse.headers[header];
492
+ try {
493
+ validateHeaderName(header);
494
+ for (const item of Is.array(value) ? value : [value]) {
495
+ if (!Is.empty(item)) {
496
+ validateHeaderValue(header, item);
497
+ }
498
+ }
499
+ }
500
+ catch (err) {
501
+ throw new GeneralError(BaseServer.CLASS_NAME, "invalidHeader", { header }, err);
502
+ }
503
+ }
504
+ }
505
+ }
506
+ /**
507
+ * Build the context ids for an incoming request.
508
+ * @param requestOrigin The origin the request was received on.
509
+ * @param headers The request headers.
510
+ * @returns The context ids.
511
+ */
512
+ buildContextIds(requestOrigin, headers) {
513
+ return {
514
+ [HttpContextIdKeys.IpAddress]: HeaderHelper.extractClientIps(headers).join("|"),
515
+ [HttpContextIdKeys.UserAgent]: HeaderHelper.extractUserAgent(headers),
516
+ [HttpContextIdKeys.CorrelationId]: HeaderHelper.extractCorrelationId(headers),
517
+ [HttpContextIdKeys.RemoteRequest]: RandomHelper.generateUuidV7("compact"),
518
+ [HttpContextIdKeys.LocalOrigin]: this._localOrigin,
519
+ // This can be overridden by a processor if needed, for example a tenant processor
520
+ [HttpContextIdKeys.PublicOrigin]: this._publicOrigin ?? requestOrigin ?? this._localOrigin
521
+ };
522
+ }
523
+ /**
524
+ * Run the REST processors for the route.
525
+ * @param restRoute The route to process.
526
+ * @param httpServerRequest The incoming request.
527
+ * @param httpResponse The outgoing response.
528
+ * @param contextIds The context IDs of the request.
529
+ * @param processorState The state handed through the processors.
530
+ */
531
+ async runProcessorsRest(restRoute, httpServerRequest, httpResponse, contextIds, processorState) {
532
+ let hasPreError = false;
533
+ try {
534
+ // Run inside ContextIdStore.run so pre-processors can do tenant-scoped storage lookups.
535
+ await ContextIdStore.run(contextIds, async () => {
536
+ for (const pre of this._restChains.pre) {
537
+ await pre(httpServerRequest, httpResponse, restRoute, contextIds, processorState, {
538
+ loggingComponentType: this._loggingComponentType
539
+ });
540
+ }
541
+ });
542
+ }
543
+ catch (err) {
544
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
545
+ HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode, this._includeErrorStack);
546
+ hasPreError = true;
547
+ }
548
+ // A pre-processor may set an error response without throwing; treat that as halt.
549
+ if (!hasPreError &&
550
+ Is.integer(httpResponse.statusCode) &&
551
+ httpResponse.statusCode >= HttpStatusCode.badRequest) {
552
+ hasPreError = true;
553
+ }
554
+ // Don't run the main processing if there was an error in the pre processing
555
+ // As this is likely to perform tasks such as authentication which may have failed
556
+ if (!hasPreError) {
557
+ try {
558
+ // Run the processors within an async context
559
+ // so that any services can access the context ids
560
+ await ContextIdStore.run(contextIds, async () => {
561
+ for (const process of this._restChains.process) {
562
+ await process(httpServerRequest, httpResponse, restRoute, processorState, {
563
+ loggingComponentType: this._loggingComponentType
564
+ });
565
+ }
566
+ });
567
+ }
568
+ catch (err) {
569
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
570
+ HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode, this._includeErrorStack);
571
+ }
572
+ }
573
+ try {
574
+ // Always run the post processors, even if there was an error earlier
575
+ // as they may perform cleanup tasks, or logging etc
576
+ await ContextIdStore.run(contextIds, async () => {
577
+ for (const post of this._restChains.post) {
578
+ await post(httpServerRequest, httpResponse, restRoute, contextIds, processorState, {
579
+ loggingComponentType: this._loggingComponentType
580
+ });
581
+ }
582
+ });
583
+ }
584
+ catch (err) {
585
+ // Just log post processor errors
586
+ await this._logging?.log({
587
+ level: "error",
588
+ ts: Date.now(),
589
+ source: BaseServer.CLASS_NAME,
590
+ message: "postProcessorError",
591
+ error: BaseError.fromError(err),
592
+ data: {
593
+ route: restRoute?.path ?? ""
594
+ }
595
+ });
596
+ }
597
+ }
598
+ /**
599
+ * Run the socket processors for the route.
600
+ * @param socketRoute The route to process.
601
+ * @param socketServerRequest The incoming request.
602
+ * @param httpResponse The outgoing response.
603
+ * @param contextIds The context IDs of the request.
604
+ * @param processorState The state handed through the processors.
605
+ * @param requestTopic The topic of the request.
606
+ * @param responseEmitter The emitter to send the response on.
607
+ */
608
+ async runProcessorsSocket(socketRoute, socketServerRequest, httpResponse, contextIds, processorState, requestTopic, responseEmitter) {
609
+ // Custom emit method which will also call the post processors
610
+ const postProcessEmit = async (topic, response, responseProcessorState) => {
611
+ await responseEmitter(topic, response);
612
+ try {
613
+ // The post processors are called after the response has been emitted
614
+ for (const post of this._socketChains.post) {
615
+ await post(socketServerRequest, response, socketRoute, contextIds, responseProcessorState, {
616
+ loggingComponentType: this._loggingComponentType
617
+ });
618
+ }
619
+ }
620
+ catch (err) {
621
+ await this._logging?.log({
622
+ level: "error",
623
+ ts: Date.now(),
624
+ source: BaseServer.CLASS_NAME,
625
+ message: "postProcessorError",
626
+ error: BaseError.fromError(err),
627
+ data: {
628
+ route: socketRoute.path
629
+ }
630
+ });
631
+ }
632
+ };
633
+ try {
634
+ for (const pre of this._socketChains.pre) {
635
+ await pre(socketServerRequest, httpResponse, socketRoute, contextIds, processorState, {
636
+ loggingComponentType: this._loggingComponentType
637
+ });
638
+ }
639
+ // We always call all the processors regardless of any response set by a previous processor.
640
+ // But if a pre processor sets a status code, we will emit the response manually, as the pre
641
+ // and post processors do not receive the emit method, they just populate the response object.
642
+ if (!Is.empty(httpResponse.statusCode)) {
643
+ await postProcessEmit(requestTopic, httpResponse, processorState);
644
+ }
645
+ await ContextIdStore.run(contextIds, async () => {
646
+ for (const process of this._socketChains.process) {
647
+ await process(socketServerRequest, httpResponse, socketRoute, processorState, async (topic, processResponse) => {
648
+ await postProcessEmit(topic, processResponse, processorState);
649
+ }, this._loggingComponentType);
650
+ }
651
+ });
652
+ // If the processors set the status to any kind of error then we should emit this manually
653
+ if (Is.integer(httpResponse.statusCode) &&
654
+ httpResponse.statusCode >= HttpStatusCode.badRequest) {
655
+ await postProcessEmit(requestTopic, httpResponse, processorState);
656
+ }
657
+ }
658
+ catch (err) {
659
+ // Emit any unhandled errors manually
660
+ const { error, httpStatusCode } = HttpErrorHelper.processError(err, this._includeErrorStack);
661
+ HttpErrorHelper.buildResponse(httpResponse, error, httpStatusCode, this._includeErrorStack);
662
+ await postProcessEmit(requestTopic, httpResponse, processorState);
663
+ }
664
+ }
665
+ }
666
+ //# sourceMappingURL=baseServer.js.map