qhttpx 1.8.5 → 1.8.11

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 (161) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +72 -52
  3. package/binding.gyp +18 -0
  4. package/dist/examples/api-server.js +29 -8
  5. package/dist/examples/basic.d.ts +1 -0
  6. package/dist/examples/basic.js +10 -0
  7. package/dist/examples/compression.d.ts +1 -0
  8. package/dist/examples/compression.js +17 -0
  9. package/dist/examples/cors.d.ts +1 -0
  10. package/dist/examples/cors.js +19 -0
  11. package/dist/examples/errors.d.ts +1 -0
  12. package/dist/examples/errors.js +25 -0
  13. package/dist/examples/file-upload.d.ts +1 -0
  14. package/dist/examples/file-upload.js +24 -0
  15. package/dist/examples/fusion.d.ts +1 -0
  16. package/dist/examples/fusion.js +21 -0
  17. package/dist/examples/rate-limiting.d.ts +1 -0
  18. package/dist/examples/rate-limiting.js +17 -0
  19. package/dist/examples/validation.d.ts +1 -0
  20. package/dist/examples/validation.js +23 -0
  21. package/dist/examples/websockets.d.ts +1 -0
  22. package/dist/examples/websockets.js +20 -0
  23. package/dist/package.json +11 -1
  24. package/dist/src/benchmarks/simple-json.js +6 -4
  25. package/dist/src/cli/index.js +33 -11
  26. package/dist/src/core/errors.d.ts +34 -0
  27. package/dist/src/core/errors.js +70 -0
  28. package/dist/src/core/native-adapter.d.ts +11 -0
  29. package/dist/src/core/native-adapter.js +211 -0
  30. package/dist/src/core/server.d.ts +52 -4
  31. package/dist/src/core/server.js +389 -261
  32. package/dist/src/core/types.d.ts +37 -0
  33. package/dist/src/index.d.ts +6 -1
  34. package/dist/src/index.js +19 -3
  35. package/dist/src/middleware/compression.d.ts +1 -5
  36. package/dist/src/middleware/cors.d.ts +1 -10
  37. package/dist/src/middleware/presets.d.ts +4 -1
  38. package/dist/src/middleware/presets.js +22 -3
  39. package/dist/src/middleware/rate-limit.d.ts +1 -19
  40. package/dist/src/middleware/rate-limit.js +6 -0
  41. package/dist/src/middleware/security.d.ts +1 -2
  42. package/dist/src/native/index.d.ts +29 -0
  43. package/dist/src/native/index.js +64 -0
  44. package/dist/src/router/radix-tree.d.ts +2 -0
  45. package/dist/src/router/radix-tree.js +54 -4
  46. package/dist/src/router/router.d.ts +1 -0
  47. package/dist/src/router/router.js +42 -2
  48. package/dist/tests/native-adapter.test.d.ts +1 -0
  49. package/dist/tests/native-adapter.test.js +71 -0
  50. package/dist/tests/resources.test.js +3 -0
  51. package/dist/tests/security.test.js +2 -2
  52. package/docs/AEGIS.md +34 -9
  53. package/docs/BENCHMARKS.md +8 -7
  54. package/docs/ERRORS.md +112 -0
  55. package/docs/FUSION.md +68 -0
  56. package/docs/MIDDLEWARE.md +65 -0
  57. package/docs/ROUTING.md +70 -0
  58. package/docs/STATIC.md +61 -0
  59. package/docs/WEBSOCKETS.md +76 -0
  60. package/package.json +11 -1
  61. package/src/native/README.md +31 -0
  62. package/src/native/addon.cc +8 -0
  63. package/src/native/index.ts +78 -0
  64. package/src/native/picohttpparser.c +608 -0
  65. package/src/native/picohttpparser.h +71 -0
  66. package/src/native/server.cc +262 -0
  67. package/src/native/server.h +30 -0
  68. package/.eslintrc.json +0 -22
  69. package/.github/workflows/ci.yml +0 -32
  70. package/.github/workflows/npm-publish.yml +0 -37
  71. package/.github/workflows/release.yml +0 -21
  72. package/.prettierrc +0 -7
  73. package/assets/logo.svg +0 -25
  74. package/eslint.config.cjs +0 -26
  75. package/examples/api-server.ts +0 -62
  76. package/src/benchmarks/quantam-users.ts +0 -70
  77. package/src/benchmarks/simple-json.ts +0 -71
  78. package/src/benchmarks/ultra-mode.ts +0 -127
  79. package/src/cli/index.ts +0 -214
  80. package/src/client/index.ts +0 -93
  81. package/src/core/batch.ts +0 -110
  82. package/src/core/body-parser.ts +0 -151
  83. package/src/core/buffer-pool.ts +0 -96
  84. package/src/core/config.ts +0 -60
  85. package/src/core/fusion.ts +0 -210
  86. package/src/core/logger.ts +0 -70
  87. package/src/core/metrics.ts +0 -166
  88. package/src/core/resources.ts +0 -38
  89. package/src/core/scheduler.ts +0 -126
  90. package/src/core/scope.ts +0 -87
  91. package/src/core/serializer.ts +0 -41
  92. package/src/core/server.ts +0 -1234
  93. package/src/core/stream.ts +0 -111
  94. package/src/core/tasks.ts +0 -138
  95. package/src/core/types.ts +0 -192
  96. package/src/core/websocket.ts +0 -112
  97. package/src/core/worker-queue.ts +0 -90
  98. package/src/database/adapters/memory.ts +0 -99
  99. package/src/database/adapters/mongo.ts +0 -116
  100. package/src/database/adapters/postgres.ts +0 -86
  101. package/src/database/adapters/sqlite.ts +0 -44
  102. package/src/database/coalescer.ts +0 -153
  103. package/src/database/manager.ts +0 -97
  104. package/src/database/types.ts +0 -24
  105. package/src/index.ts +0 -58
  106. package/src/middleware/compression.ts +0 -147
  107. package/src/middleware/cors.ts +0 -98
  108. package/src/middleware/presets.ts +0 -50
  109. package/src/middleware/rate-limit.ts +0 -106
  110. package/src/middleware/security.ts +0 -109
  111. package/src/middleware/static.ts +0 -216
  112. package/src/openapi/generator.ts +0 -167
  113. package/src/router/radix-router.ts +0 -119
  114. package/src/router/radix-tree.ts +0 -106
  115. package/src/router/router.ts +0 -190
  116. package/src/testing/index.ts +0 -104
  117. package/src/utils/cookies.ts +0 -67
  118. package/src/utils/logger.ts +0 -59
  119. package/src/utils/signals.ts +0 -45
  120. package/src/utils/sse.ts +0 -41
  121. package/src/validation/index.ts +0 -3
  122. package/src/validation/simple.ts +0 -93
  123. package/src/validation/types.ts +0 -38
  124. package/src/validation/zod.ts +0 -14
  125. package/src/views/index.ts +0 -1
  126. package/src/views/types.ts +0 -4
  127. package/tests/adapters.test.ts +0 -120
  128. package/tests/batch.test.ts +0 -139
  129. package/tests/body-parser.test.ts +0 -83
  130. package/tests/compression-sse.test.ts +0 -98
  131. package/tests/cookies.test.ts +0 -74
  132. package/tests/cors.test.ts +0 -79
  133. package/tests/database.test.ts +0 -90
  134. package/tests/dx.test.ts +0 -130
  135. package/tests/ecosystem.test.ts +0 -156
  136. package/tests/features.test.ts +0 -51
  137. package/tests/fusion.test.ts +0 -121
  138. package/tests/http-basic.test.ts +0 -161
  139. package/tests/logger.test.ts +0 -48
  140. package/tests/middleware.test.ts +0 -137
  141. package/tests/observability.test.ts +0 -91
  142. package/tests/openapi.test.ts +0 -74
  143. package/tests/plugin.test.ts +0 -85
  144. package/tests/plugins.test.ts +0 -93
  145. package/tests/rate-limit.test.ts +0 -97
  146. package/tests/resources.test.ts +0 -64
  147. package/tests/scheduler.test.ts +0 -71
  148. package/tests/schema-routes.test.ts +0 -89
  149. package/tests/security.test.ts +0 -128
  150. package/tests/server-db.test.ts +0 -72
  151. package/tests/smoke.test.ts +0 -9
  152. package/tests/sqlite-fusion.test.ts +0 -106
  153. package/tests/static.test.ts +0 -111
  154. package/tests/stream.test.ts +0 -58
  155. package/tests/task-metrics.test.ts +0 -78
  156. package/tests/tasks.test.ts +0 -90
  157. package/tests/testing.test.ts +0 -53
  158. package/tests/validation.test.ts +0 -126
  159. package/tests/websocket.test.ts +0 -132
  160. package/tsconfig.json +0 -17
  161. package/vitest.config.ts +0 -9
@@ -1,1234 +0,0 @@
1
- import http, { IncomingMessage, ServerResponse } from 'http';
2
- import path from 'path';
3
- import type { Duplex } from 'stream';
4
- import { URL } from 'url';
5
- import pkgJson from '../../package.json';
6
- import { Router } from '../router/router';
7
- import { Scheduler } from './scheduler';
8
- import { TaskEngine } from './tasks';
9
- import { calculateWorkerCount, isResourceOverloaded } from './resources';
10
- import { Metrics } from './metrics';
11
- import { BodyParser } from './body-parser';
12
- import { BufferPool } from './buffer-pool';
13
- import { WebSocketManager } from './websocket';
14
- import { fastJsonStringify, getStringifier } from './serializer';
15
- import { BatchExecutor } from './batch';
16
- import { RequestFusion } from './fusion';
17
- import { SimpleValidator } from '../validation/simple';
18
- import { RouteSchema, Validator } from '../validation/types';
19
- import { OpenAPIGenerator, OpenAPIOptions } from '../openapi/generator';
20
- import {
21
- HTTPMethod,
22
- HttpError,
23
- QHTTPXContext,
24
- QHTTPXErrorHandler,
25
- QHTTPXHandler,
26
- QHTTPXMethodNotAllowedHandler,
27
- QHTTPXMiddleware,
28
- QHTTPXNotFoundHandler,
29
- QHTTPXOptions,
30
- QHTTPXRouteOptions,
31
- QHTTPXTracer,
32
- RoutePriority,
33
- QHTTPXPlugin,
34
- QHTTPXPluginOptions,
35
- CookieOptions,
36
- } from './types';
37
-
38
- import { parseCookies, serializeCookie } from '../utils/cookies';
39
- import { QHTTPXScope } from './scope';
40
- import { Logger } from './logger';
41
-
42
- export class QHTTPX {
43
- private readonly options: QHTTPXOptions;
44
-
45
- private readonly server: http.Server;
46
-
47
- public readonly logger: Logger;
48
-
49
- private readonly router: Router;
50
-
51
- private readonly scheduler: Scheduler;
52
-
53
- private readonly middlewares: QHTTPXMiddleware[] = [];
54
-
55
- private readonly workerCount: number;
56
-
57
- private readonly tasks: TaskEngine;
58
-
59
- private readonly metrics: Metrics;
60
-
61
- private readonly contextPool: QHTTPXContext[] = [];
62
-
63
- private readonly bufferPool: BufferPool;
64
-
65
- private pipelineRunner: ((ctx: QHTTPXContext, handler: QHTTPXHandler) => Promise<void>) | null = null;
66
-
67
- private errorHandler?: QHTTPXErrorHandler;
68
-
69
- private notFoundHandler?: QHTTPXNotFoundHandler;
70
-
71
- private methodNotAllowedHandler?: QHTTPXMethodNotAllowedHandler;
72
-
73
- private readonly tracer?: QHTTPXTracer;
74
-
75
- private readonly onStartHooks: Array<() => void | Promise<void>> = [];
76
-
77
- private readonly onBeforeShutdownHooks: Array<() => void | Promise<void>> = [];
78
-
79
- private readonly onShutdownHooks: Array<() => void | Promise<void>> = [];
80
-
81
- private nextRequestId = 1;
82
-
83
- private readonly wsManager: WebSocketManager;
84
-
85
- private readonly ultraMode: boolean;
86
-
87
- private readonly batchExecutor?: BatchExecutor;
88
-
89
- private readonly fusion?: RequestFusion;
90
-
91
- private readonly validator: Validator;
92
-
93
- constructor(options: QHTTPXOptions = {}) {
94
- this.options = options;
95
- this.logger = new Logger({
96
- name: options.name,
97
- level: options.performanceMode === 'ultra' ? 'error' : 'info'
98
- });
99
- this.ultraMode = options.performanceMode === 'ultra';
100
- this.errorHandler = this.ultraMode ? undefined : options.errorHandler;
101
- this.notFoundHandler = this.ultraMode ? undefined : options.notFoundHandler;
102
- this.methodNotAllowedHandler = this.ultraMode ? undefined : options.methodNotAllowedHandler;
103
- this.tracer = this.ultraMode ? undefined : options.tracer;
104
- this.workerCount = calculateWorkerCount(options.workers ?? 'auto');
105
- this.router = new Router();
106
- this.wsManager = new WebSocketManager(this.generateRequestId.bind(this));
107
- this.bufferPool = new BufferPool(options.bufferPoolConfig);
108
- const maxConcurrency = options.maxConcurrency ?? 1024;
109
- this.scheduler = new Scheduler({
110
- maxConcurrency,
111
- });
112
- this.tasks = new TaskEngine(this.scheduler);
113
-
114
- if (options.enableBatching) {
115
- this.batchExecutor = new BatchExecutor(options.database);
116
- }
117
-
118
- if (options.enableRequestFusion) {
119
- this.fusion = new RequestFusion(options.enableRequestFusion);
120
- }
121
-
122
- this.validator = options.validator ?? new SimpleValidator();
123
-
124
- this.metrics = new Metrics(
125
- this.scheduler,
126
- {
127
- enabled: !this.ultraMode && (this.options.metricsEnabled ?? true),
128
- },
129
- this.tasks,
130
- );
131
- for (let i = 0; i < maxConcurrency; i += 1) {
132
- this.contextPool.push(this.createContext());
133
- }
134
- this.registerInternalRoutes();
135
- this.compileMiddlewarePipeline();
136
- this.server = http.createServer(this.handleRequest.bind(this));
137
- if (this.options.keepAliveTimeoutMs !== undefined) {
138
- this.server.keepAliveTimeout = this.options.keepAliveTimeoutMs;
139
- } else if (this.ultraMode) {
140
- this.server.keepAliveTimeout = 0;
141
- }
142
- if (this.options.headersTimeoutMs !== undefined) {
143
- this.server.headersTimeout = this.options.headersTimeoutMs;
144
- } else if (this.ultraMode) {
145
- this.server.headersTimeout = 0;
146
- }
147
- if (this.ultraMode) {
148
- this.server.requestTimeout = 0;
149
- this.server.maxHeadersCount = 0;
150
- this.server.timeout = 0;
151
- }
152
- this.server.on('upgrade', (req, socket, head) => {
153
- void this.handleUpgrade(req, socket, head);
154
- });
155
- }
156
-
157
- get serverInstance(): http.Server {
158
- return this.server;
159
- }
160
-
161
- get websocket(): WebSocketManager {
162
- return this.wsManager;
163
- }
164
-
165
- setErrorHandler(handler: QHTTPXErrorHandler): void {
166
- this.errorHandler = handler;
167
- }
168
-
169
- setNotFoundHandler(handler: QHTTPXNotFoundHandler): void {
170
- this.notFoundHandler = handler;
171
- }
172
-
173
- setMethodNotAllowedHandler(handler: QHTTPXMethodNotAllowedHandler): void {
174
- this.methodNotAllowedHandler = handler;
175
- }
176
-
177
- set404Handler(handler: QHTTPXNotFoundHandler): void {
178
- this.setNotFoundHandler(handler);
179
- }
180
-
181
- set405Handler(handler: QHTTPXMethodNotAllowedHandler): void {
182
- this.setMethodNotAllowedHandler(handler);
183
- }
184
-
185
- /**
186
- * Alias for setErrorHandler
187
- */
188
- onError(handler: QHTTPXErrorHandler): void {
189
- this.setErrorHandler(handler);
190
- }
191
-
192
- /**
193
- * Alias for setNotFoundHandler
194
- */
195
- notFound(handler: QHTTPXNotFoundHandler): void {
196
- this.setNotFoundHandler(handler);
197
- }
198
-
199
- onStart(hook: () => void | Promise<void>): void {
200
- this.onStartHooks.push(hook);
201
- }
202
-
203
- onBeforeShutdown(hook: () => void | Promise<void>): void {
204
- this.onBeforeShutdownHooks.push(hook);
205
- }
206
-
207
- onShutdown(hook: () => void | Promise<void>): void {
208
- this.onShutdownHooks.push(hook);
209
- }
210
-
211
- upgrade(
212
- path: string,
213
- handler: import('./websocket').WSHandler,
214
- ): void {
215
- this.wsManager.register(path, handler);
216
- }
217
-
218
- use(middleware: QHTTPXMiddleware): void {
219
- this.middlewares.push(middleware);
220
- // Recompile pipeline after middleware is added
221
- this.compileMiddlewarePipeline();
222
- }
223
-
224
- private compileMiddlewarePipeline(): void {
225
- const middlewares = this.middlewares;
226
-
227
- if (middlewares.length === 0) {
228
- this.pipelineRunner = async (ctx, handler) => {
229
- const result = handler(ctx);
230
- if (result && typeof (result as Promise<void>).then === 'function') {
231
- await result;
232
- }
233
- };
234
- return;
235
- }
236
-
237
- // Flatten middleware pipeline into a single function chain
238
- // This eliminates Promise nesting, recursive dispatch overhead, and microtask backlogs
239
- // Each middleware executes directly without closure allocation overhead
240
- this.pipelineRunner = async (ctx, handler) => {
241
- let index = 0;
242
-
243
- const executeNext = async (): Promise<void> => {
244
- if (index >= middlewares.length) {
245
- // All middlewares done, execute handler
246
- const result = handler(ctx);
247
- if (result && typeof (result as unknown as Record<string, unknown>).then === 'function') {
248
- await (result as Promise<void>);
249
- }
250
- return;
251
- }
252
-
253
- const currentIndex = index;
254
- index += 1;
255
-
256
- const result = middlewares[currentIndex](ctx, executeNext);
257
- if (result && typeof (result as unknown as Record<string, unknown>).then === 'function') {
258
- await (result as Promise<void>);
259
- }
260
- };
261
-
262
- await executeNext();
263
- };
264
- }
265
-
266
- private compileRoutePipeline(
267
- handler: QHTTPXHandler,
268
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
269
- schema?: RouteSchema | Record<string, any>,
270
- ): QHTTPXHandler {
271
- const middlewares = this.middlewares;
272
-
273
- // Heuristic: Is it a structured RouteSchema or a legacy ResponseSchema?
274
- let responseSchema: unknown | undefined;
275
- let requestSchema: RouteSchema | undefined;
276
-
277
- if (schema) {
278
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
- const s = schema as any;
280
- if (s.body || s.query || s.params || s.headers || s.response) {
281
- // It is a RouteSchema
282
- requestSchema = s;
283
- responseSchema = s.response;
284
- } else {
285
- // It is a legacy ResponseSchema
286
- responseSchema = s;
287
- }
288
- }
289
-
290
- const stringifier = responseSchema ? getStringifier(responseSchema) : undefined;
291
-
292
- // Build middleware chain
293
- let pipeline = handler;
294
-
295
- // Wrap with validation if needed (before handler, after global middleware)
296
- if (requestSchema) {
297
- const inner = pipeline;
298
- pipeline = async (ctx) => {
299
- // Validate Body
300
- if (requestSchema!.body) {
301
- const result = await this.validator.validate(requestSchema!.body, ctx.body);
302
- if (!result.success) {
303
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
- const err = result.error as any;
305
- ctx.json({ error: 'Validation Error', details: err.details || err.message || err }, 400);
306
- return;
307
- }
308
- ctx.body = result.data;
309
- }
310
-
311
- // Validate Query
312
- if (requestSchema!.query) {
313
- const result = await this.validator.validate(requestSchema!.query, ctx.query);
314
- if (!result.success) {
315
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
316
- const err = result.error as any;
317
- ctx.json({ error: 'Validation Error', details: err.details || err.message || err }, 400);
318
- return;
319
- }
320
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
321
- (ctx as any).query = result.data;
322
- }
323
-
324
- // Validate Params
325
- if (requestSchema!.params) {
326
- const result = await this.validator.validate(requestSchema!.params, ctx.params);
327
- if (!result.success) {
328
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
329
- const err = result.error as any;
330
- ctx.json({ error: 'Validation Error', details: err.details || err.message || err }, 400);
331
- return;
332
- }
333
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
334
- (ctx as any).params = result.data;
335
- }
336
-
337
- return inner(ctx);
338
- };
339
- }
340
-
341
- // Wrap with Request Fusion if enabled
342
- if (this.fusion) {
343
- const inner = pipeline;
344
- pipeline = (ctx) => this.fusion!.coalesce(ctx, inner);
345
- }
346
-
347
- // We iterate backwards to wrap the handler
348
- // pipeline = m[last](ctx, () => pipeline(ctx))
349
- for (let i = middlewares.length - 1; i >= 0; i -= 1) {
350
- const middleware = middlewares[i];
351
- const next = pipeline;
352
- pipeline = (ctx) => {
353
- const nextFn = async () => {
354
- const result = next(ctx);
355
- if (result && typeof (result as Promise<void>).then === 'function') {
356
- await result;
357
- }
358
- };
359
- // Attach next to ctx for destructuring support
360
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
361
- (ctx as any).next = nextFn;
362
-
363
- return middleware(ctx, nextFn);
364
- };
365
- }
366
-
367
- if (stringifier) {
368
- const inner = pipeline;
369
- return (ctx) => {
370
- ctx.serializer = stringifier;
371
- return inner(ctx);
372
- };
373
- }
374
-
375
- return pipeline;
376
- }
377
-
378
- // Internal registration to be accessed by Scopes
379
- public _registerRoute(
380
- method: HTTPMethod,
381
- path: string,
382
- handlerOrOptions: QHTTPXHandler | QHTTPXRouteOptions,
383
- ): void {
384
- this.registerRoute(method, path, handlerOrOptions);
385
- }
386
-
387
- public async register<Options extends QHTTPXPluginOptions>(
388
- plugin: QHTTPXPlugin<Options>,
389
- options?: Options
390
- ): Promise<void> {
391
- const scope = new QHTTPXScope(this, options?.prefix);
392
- await plugin(scope, options as Options);
393
- }
394
-
395
- private registerRoute(
396
- method: HTTPMethod,
397
- path: string,
398
- handlerOrOptions:
399
- | QHTTPXHandler
400
- | QHTTPXRouteOptions
401
- | import('./types').QHTTPXRouteConfig,
402
- handlerIfOptions?: QHTTPXHandler,
403
- ): void {
404
- let handler: QHTTPXHandler;
405
- let schema: RouteSchema | Record<string, unknown> | undefined;
406
- const options: { priority?: RoutePriority } = {};
407
-
408
- if (typeof handlerOrOptions === 'function') {
409
- handler = handlerOrOptions;
410
- } else if (handlerIfOptions) {
411
- handler = handlerIfOptions;
412
- schema = handlerOrOptions.schema;
413
- options.priority = handlerOrOptions.priority;
414
- } else {
415
- const opts = handlerOrOptions as QHTTPXRouteOptions;
416
- handler = opts.handler;
417
- schema = opts.schema;
418
- options.priority = opts.priority;
419
- }
420
-
421
- const compiled = this.compileRoutePipeline(handler, schema);
422
- this.router.register(method, path, compiled, { ...options, schema });
423
- }
424
-
425
- get(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
426
- get(
427
- path: string,
428
- config: import('./types').QHTTPXRouteConfig,
429
- handler: QHTTPXHandler,
430
- ): void;
431
- get(
432
- path: string,
433
- handlerOrOptions:
434
- | QHTTPXHandler
435
- | QHTTPXRouteOptions
436
- | import('./types').QHTTPXRouteConfig,
437
- handler?: QHTTPXHandler,
438
- ): void {
439
- this.registerRoute('GET', path, handlerOrOptions, handler);
440
- }
441
-
442
- post(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
443
- post(
444
- path: string,
445
- config: import('./types').QHTTPXRouteConfig,
446
- handler: QHTTPXHandler,
447
- ): void;
448
- post(
449
- path: string,
450
- handlerOrOptions:
451
- | QHTTPXHandler
452
- | QHTTPXRouteOptions
453
- | import('./types').QHTTPXRouteConfig,
454
- handler?: QHTTPXHandler,
455
- ): void {
456
- this.registerRoute('POST', path, handlerOrOptions, handler);
457
- }
458
-
459
- put(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
460
- put(
461
- path: string,
462
- config: import('./types').QHTTPXRouteConfig,
463
- handler: QHTTPXHandler,
464
- ): void;
465
- put(
466
- path: string,
467
- handlerOrOptions:
468
- | QHTTPXHandler
469
- | QHTTPXRouteOptions
470
- | import('./types').QHTTPXRouteConfig,
471
- handler?: QHTTPXHandler,
472
- ): void {
473
- this.registerRoute('PUT', path, handlerOrOptions, handler);
474
- }
475
-
476
- delete(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
477
- delete(
478
- path: string,
479
- config: import('./types').QHTTPXRouteConfig,
480
- handler: QHTTPXHandler,
481
- ): void;
482
- delete(
483
- path: string,
484
- handlerOrOptions:
485
- | QHTTPXHandler
486
- | QHTTPXRouteOptions
487
- | import('./types').QHTTPXRouteConfig,
488
- handler?: QHTTPXHandler,
489
- ): void {
490
- this.registerRoute('DELETE', path, handlerOrOptions, handler);
491
- }
492
-
493
- route(path: string) {
494
- const register = (method: HTTPMethod, handler: QHTTPXHandler | QHTTPXRouteOptions) => {
495
- this.registerRoute(method, path, handler);
496
- };
497
-
498
- const builder = {
499
- get(handler: QHTTPXHandler | QHTTPXRouteOptions) {
500
- register('GET', handler);
501
- return this;
502
- },
503
- post(handler: QHTTPXHandler | QHTTPXRouteOptions) {
504
- register('POST', handler);
505
- return this;
506
- },
507
- put(handler: QHTTPXHandler | QHTTPXRouteOptions) {
508
- register('PUT', handler);
509
- return this;
510
- },
511
- delete(handler: QHTTPXHandler | QHTTPXRouteOptions) {
512
- register('DELETE', handler);
513
- return this;
514
- },
515
- };
516
-
517
- return builder;
518
- }
519
-
520
- task(
521
- name: string,
522
- handler: import('./types').QHTTPXTaskHandler,
523
- options?: import('./types').QHTTPXTaskOptions,
524
- ): void {
525
- this.tasks.register(name, handler, options);
526
- }
527
-
528
- enqueue(name: string, payload: unknown): Promise<void> {
529
- return this.tasks.enqueue(name, payload);
530
- }
531
-
532
- op(name: string, handler: import('./types').QHTTPXOpHandler): void {
533
- if (!this.batchExecutor) {
534
- // Auto-enable batch executor if op is called?
535
- // Or throw?
536
- // Better to throw or warn if not enabled.
537
- // But for ease of use, let's create it if missing (but we miss DB context if done this way without options)
538
- // The constructor handles options. If enableBatching is false, we shouldn't be here.
539
- throw new Error('Batching is not enabled. Pass enableBatching: true to QHTTPX options.');
540
- }
541
- this.batchExecutor.register(name, handler);
542
- }
543
-
544
- private registerInternalRoutes(): void {
545
- this.router.register('GET', '/__qhttpx/health', (ctx) => {
546
- const version = typeof pkgJson.version === 'string' ? pkgJson.version : '';
547
- const name = typeof pkgJson.name === 'string' ? pkgJson.name : '';
548
-
549
- ctx.json({
550
- status: 'ok',
551
- name,
552
- version,
553
- workers: this.workerCount,
554
- });
555
- });
556
-
557
- this.router.register('GET', '/__qhttpx/metrics', (ctx) => {
558
- const snapshot = this.metrics.snapshot();
559
- ctx.json({
560
- ...snapshot,
561
- workers: this.workerCount,
562
- });
563
- });
564
-
565
- this.router.register('GET', '/__qhttpx/runtime', (ctx) => {
566
- const schedulerStats = this.scheduler.getStats();
567
- ctx.json({
568
- workers: this.workerCount,
569
- router: {
570
- frozen: this.router.isFrozenRouter(),
571
- },
572
- scheduler: schedulerStats,
573
- });
574
- });
575
-
576
- if (this.options.enableBatching && this.batchExecutor) {
577
- const endpoint = typeof this.options.enableBatching === 'object'
578
- ? this.options.enableBatching.endpoint
579
- : '/qhttpx';
580
-
581
- this.router.register('POST', endpoint, async (ctx) => {
582
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
583
- const body = ctx.body as any;
584
- if (!body || !Array.isArray(body.batch)) {
585
- ctx.json({ error: 'Invalid batch format' }, 400);
586
- return;
587
- }
588
-
589
- try {
590
- const result = await this.batchExecutor!.handleBatch(ctx, body.batch);
591
- ctx.json(result);
592
- } catch (err) {
593
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
594
- ctx.json({ error: (err as any).message }, 500);
595
- }
596
- });
597
- }
598
- }
599
-
600
- getOpenAPI(options: OpenAPIOptions): object {
601
- const generator = new OpenAPIGenerator(this.router, options);
602
- return generator.generate();
603
- }
604
-
605
- public async listen(
606
- port: number,
607
- hostnameOrCallback?: string | (() => void),
608
- callback?: () => void,
609
- ): Promise<{ port: number }> {
610
- let hostname: string | undefined;
611
- let cb: (() => void) | undefined;
612
-
613
- if (typeof hostnameOrCallback === 'function') {
614
- cb = hostnameOrCallback;
615
- hostname = undefined;
616
- } else {
617
- hostname = hostnameOrCallback;
618
- cb = callback;
619
- }
620
-
621
- if (this.options.database) {
622
- await this.options.database.connect();
623
- }
624
-
625
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
626
- if ((this.options as any).plugins) {
627
- // Plugins are registered synchronously in constructor, but their async init happens now?
628
- // Actually, plugins are registered via app.register() which is async.
629
- // So they should be ready.
630
- }
631
-
632
- return new Promise((resolve, reject) => {
633
- const onError = (error: unknown) => {
634
- this.server.off('error', onError);
635
- reject(error);
636
- };
637
-
638
- this.server.once('error', onError);
639
-
640
- this.server.listen(port, hostname, () => {
641
- this.server.off('error', onError);
642
- // Freeze router after server starts to prevent further registrations
643
- this.router.freeze();
644
- void this.runLifecycleHooks(this.onStartHooks);
645
- const address = this.server.address();
646
-
647
- if (cb) cb();
648
-
649
- if (address && typeof address === 'object') {
650
- resolve({ port: address.port });
651
- } else {
652
- resolve({ port });
653
- }
654
- });
655
- });
656
- }
657
-
658
- close(): Promise<void> {
659
- return new Promise((resolve, reject) => {
660
- this.server.close((err) => {
661
- if (err) {
662
- reject(err);
663
- return;
664
- }
665
- resolve();
666
- });
667
- });
668
- }
669
-
670
- async shutdown(): Promise<void> {
671
- await this.runLifecycleHooks(this.onBeforeShutdownHooks);
672
- await this.close();
673
- await this.runLifecycleHooks(this.onShutdownHooks);
674
- }
675
-
676
- private createContext(): QHTTPXContext {
677
- const jsonSerializer = this.options.jsonSerializer;
678
- const useFastStringify = jsonSerializer === undefined;
679
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
680
- const ctx: any = {
681
- req: null,
682
- res: null,
683
- headers: null,
684
- url: null,
685
- params: null,
686
- query: null,
687
- body: undefined,
688
- cookies: null,
689
- state: null,
690
- bufferPool: this.bufferPool,
691
- requestId: '',
692
- requestStart: 0,
693
- serializer: null,
694
- path: '',
695
- error: undefined,
696
- db: this.options.database,
697
- };
698
-
699
- // Helper to get response object from closure-captured ctx
700
- // We use arrow functions to ensure they don't depend on 'this' context at call site
701
- // enabling destructuring like: ({ json }) => json(...)
702
-
703
- ctx.json = (payload: unknown, status = 200) => {
704
- const res = ctx.res;
705
- if (!res.headersSent) {
706
- res.statusCode = status;
707
- res.setHeader('content-type', 'application/json; charset=utf-8');
708
- }
709
- let body: string | Buffer;
710
- if (ctx.serializer) {
711
- body = ctx.serializer(payload);
712
- } else if (useFastStringify) {
713
- body = fastJsonStringify(payload);
714
- } else if (jsonSerializer) {
715
- body = jsonSerializer(payload);
716
- } else {
717
- body = JSON.stringify(payload);
718
- }
719
- res.end(body);
720
- };
721
-
722
- ctx.send = (payload: string | Buffer, status = 200) => {
723
- const res = ctx.res;
724
- if (!res.headersSent) {
725
- res.statusCode = status;
726
- }
727
- res.end(payload);
728
- };
729
-
730
- ctx.html = (payload: string, status = 200) => {
731
- const res = ctx.res;
732
- if (!res.headersSent) {
733
- res.statusCode = status;
734
- res.setHeader('content-type', 'text/html; charset=utf-8');
735
- }
736
- res.end(payload);
737
- };
738
-
739
- ctx.redirect = (url: string, status = 302) => {
740
- const res = ctx.res;
741
- if (!res.headersSent) {
742
- res.statusCode = status;
743
- res.setHeader('Location', url);
744
- }
745
- res.end();
746
- };
747
-
748
- ctx.setCookie = (name: string, value: string, options: CookieOptions | undefined) => {
749
- const res = ctx.res;
750
- const serialized = serializeCookie(name, value, options);
751
- let existing = res.getHeader('Set-Cookie');
752
- if (Array.isArray(existing)) {
753
- existing.push(serialized);
754
- res.setHeader('Set-Cookie', existing);
755
- } else if (existing) {
756
- res.setHeader('Set-Cookie', [existing as string, serialized]);
757
- } else {
758
- res.setHeader('Set-Cookie', serialized);
759
- }
760
- };
761
-
762
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
763
- ctx.render = async (view: string, locals?: Record<string, any>) => {
764
- const engine = this.options.viewEngine;
765
- if (!engine) {
766
- throw new Error('No view engine registered');
767
- }
768
- const viewsPath = this.options.viewsPath || process.cwd();
769
- const fullPath = path.resolve(viewsPath, view);
770
-
771
- const html = await engine.render(fullPath, locals || {});
772
-
773
- const res = ctx.res;
774
- if (!res.headersSent) {
775
- res.statusCode = 200;
776
- res.setHeader('content-type', 'text/html; charset=utf-8');
777
- }
778
- res.end(html);
779
- };
780
-
781
- ctx.validate = async <T>(schema: unknown, data?: unknown): Promise<T> => {
782
- const target = data ?? ctx.body;
783
- const result = await this.validator.validate(schema, target);
784
- if (result.success) {
785
- return result.data as T;
786
- }
787
- throw new HttpError(400, 'Validation Error', {
788
- code: 'VALIDATION_ERROR',
789
- details: result.error,
790
- });
791
- };
792
-
793
- return ctx;
794
- }
795
-
796
- private acquireContext(
797
- req: IncomingMessage,
798
- res: ServerResponse,
799
- url: URL,
800
- params: Record<string, string>,
801
- query: Record<string, string | string[]>,
802
- requestId: string | undefined,
803
- body: unknown,
804
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
805
- files?: Record<string, any>,
806
- ): QHTTPXContext {
807
- const ctx = this.contextPool.pop() ?? this.createContext();
808
- // Reset and populate properties
809
- // We use type assertions to write to readonly/managed properties for performance
810
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
811
- const mutableCtx = ctx as any;
812
- mutableCtx.req = req;
813
- mutableCtx.res = res;
814
- mutableCtx.headers = req.headers;
815
- mutableCtx.url = url;
816
- mutableCtx.params = params;
817
- mutableCtx.query = query;
818
- mutableCtx.body = body;
819
- mutableCtx.files = files;
820
- mutableCtx.requestId = requestId;
821
- mutableCtx.serializer = undefined;
822
- // In ultra mode, skip cookie parsing to save overhead
823
- if (!this.ultraMode) {
824
- mutableCtx.cookies = parseCookies(req.headers.cookie);
825
- }
826
- mutableCtx.state = {};
827
- mutableCtx.disableAutoEnd = false;
828
- mutableCtx.path = url.pathname;
829
-
830
- return ctx;
831
- }
832
-
833
- private releaseContext(ctx: QHTTPXContext) {
834
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
835
- const mutableCtx = ctx as any;
836
- mutableCtx.req = null;
837
- mutableCtx.res = null;
838
- mutableCtx.headers = null;
839
- mutableCtx.url = null;
840
- mutableCtx.params = null;
841
- mutableCtx.query = null;
842
- mutableCtx.body = undefined;
843
- mutableCtx.files = undefined;
844
- mutableCtx.requestId = '';
845
- mutableCtx.requestStart = 0;
846
- mutableCtx.serializer = null;
847
- mutableCtx.cookies = null;
848
- mutableCtx.state = null;
849
- mutableCtx.path = '';
850
- mutableCtx.error = undefined;
851
- // render method is static per context instance creation (closure over options),
852
- // but good to keep it consistent.
853
- // Wait, 'render' is defined in 'createContext' and depends on 'this.options'.
854
- // It doesn't hold request-specific state other than 'res' which is updated in 'acquireContext'.
855
- // So we don't need to null it out.
856
-
857
- if (this.contextPool.length < (this.options.maxConcurrency ?? 1024)) {
858
- this.contextPool.push(ctx);
859
- }
860
- }
861
-
862
- private async handleRequest(req: IncomingMessage, res: ServerResponse) {
863
- const rawMethod = (req.method || 'GET').toUpperCase();
864
- const method = rawMethod as HTTPMethod;
865
- const rawUrl = req.url || '/';
866
- const host = req.headers.host || 'localhost';
867
- const url = new URL(rawUrl, `http://${host}`);
868
-
869
- const incomingRequestIdHeader = req.headers['x-request-id'];
870
- let requestId: string | undefined;
871
- if (typeof incomingRequestIdHeader === 'string') {
872
- requestId = incomingRequestIdHeader;
873
- } else if (Array.isArray(incomingRequestIdHeader)) {
874
- requestId = incomingRequestIdHeader[0];
875
- }
876
- if (!requestId) {
877
- requestId = this.generateRequestId();
878
- }
879
- if (!res.headersSent && requestId) {
880
- res.setHeader('x-request-id', requestId);
881
- }
882
-
883
- let match = this.router.match(method, url.pathname);
884
- const hasRoute = !!match;
885
-
886
- if (!match) {
887
- match = {
888
- handler: () => {},
889
- params: {},
890
- priority: RoutePriority.STANDARD,
891
- };
892
- }
893
-
894
- const allowedMethods = this.router.getAllowedMethods(url.pathname);
895
-
896
- if (!hasRoute && !res.headersSent) {
897
- const hasAnyMethod = allowedMethods.length > 0;
898
- res.statusCode = hasAnyMethod ? 405 : 404;
899
- }
900
-
901
- const query: Record<string, string | string[]> = {};
902
- url.searchParams.forEach((value, key) => {
903
- if (Object.prototype.hasOwnProperty.call(query, key)) {
904
- const existing = query[key];
905
- if (Array.isArray(existing)) {
906
- existing.push(value);
907
- } else {
908
- query[key] = [existing, value];
909
- }
910
- } else {
911
- query[key] = value;
912
- }
913
- });
914
-
915
- if (this.options.maxMemoryBytes !== undefined) {
916
- const sampleRss = process.memoryUsage().rss;
917
- const overloadedByMemory = isResourceOverloaded(
918
- { rssBytes: sampleRss },
919
- { maxRssBytes: this.options.maxMemoryBytes },
920
- );
921
- if (overloadedByMemory) {
922
- const overloaded = () => {
923
- if (res.writableEnded) {
924
- return;
925
- }
926
- if (!res.headersSent) {
927
- res.statusCode = 503;
928
- res.setHeader('content-type', 'text/plain; charset=utf-8');
929
- }
930
- res.end('Server overloaded');
931
- };
932
- overloaded();
933
- return;
934
- }
935
- }
936
-
937
- let body: unknown;
938
- let files: Record<string, unknown> | undefined;
939
-
940
- if (method !== 'GET' && method !== 'HEAD') {
941
- try {
942
- const parsed = await BodyParser.parse(req, {
943
- maxBodyBytes: this.options.maxBodyBytes,
944
- });
945
- body = parsed.body;
946
- files = parsed.files;
947
- } catch (err) {
948
- if (err instanceof Error && err.message === 'QHTTPX_INVALID_JSON') {
949
- if (!res.headersSent) {
950
- res.statusCode = 400;
951
- res.setHeader('content-type', 'text/plain; charset=utf-8');
952
- }
953
- res.end('Invalid JSON');
954
- return;
955
- }
956
- if (err instanceof Error && err.message === 'QHTTPX_BODY_TOO_LARGE') {
957
- if (!res.headersSent) {
958
- res.statusCode = 413;
959
- res.setHeader('content-type', 'text/plain; charset=utf-8');
960
- }
961
- res.end('Payload Too Large');
962
- return;
963
- }
964
-
965
- if (!res.headersSent) {
966
- res.statusCode = 500;
967
- res.setHeader('content-type', 'text/plain; charset=utf-8');
968
- }
969
- res.end('Internal Server Error');
970
- return;
971
- }
972
- }
973
-
974
- const ctx = this.acquireContext(
975
- req,
976
- res,
977
- url,
978
- match.params,
979
- query,
980
- requestId,
981
- body,
982
- files,
983
- );
984
-
985
- const overloaded = () => {
986
- if (res.writableEnded) {
987
- return;
988
- }
989
- if (!res.headersSent) {
990
- res.statusCode = 503;
991
- res.setHeader('content-type', 'text/plain; charset=utf-8');
992
- }
993
- res.end('Server overloaded');
994
- };
995
-
996
- let timedOut = false;
997
-
998
- const onTimeout = () => {
999
- timedOut = true;
1000
- if (res.writableEnded) {
1001
- return;
1002
- }
1003
- if (!res.headersSent) {
1004
- res.statusCode = 504;
1005
- res.setHeader('content-type', 'text/plain; charset=utf-8');
1006
- }
1007
- res.end('Request timed out');
1008
- };
1009
-
1010
- const start = Date.now();
1011
- ctx.requestStart = start;
1012
- if (!this.ultraMode) {
1013
- this.metrics.onRequestStart();
1014
- }
1015
-
1016
- if (this.tracer) {
1017
- const event = {
1018
- type: 'request_start' as const,
1019
- method,
1020
- path: url.pathname,
1021
- requestId,
1022
- };
1023
- const result = this.tracer(event);
1024
- if (result && typeof (result as Promise<void>).then === 'function') {
1025
- void result;
1026
- }
1027
- }
1028
-
1029
- const handle = async () => {
1030
- const handler = match.handler;
1031
-
1032
- try {
1033
- if (hasRoute) {
1034
- // Optimized path: Route handler is already compiled with middlewares
1035
- const result = handler(ctx);
1036
- if (result && typeof (result as Promise<void>).then === 'function') {
1037
- await result;
1038
- }
1039
- } else if (this.pipelineRunner) {
1040
- // Slow path: Run global middleware pipeline for 404/405
1041
- await this.pipelineRunner(ctx, handler);
1042
- }
1043
-
1044
- if (!res.writableEnded) {
1045
- if (!hasRoute) {
1046
- const hasAnyMethod = allowedMethods.length > 0;
1047
- if (hasAnyMethod && this.methodNotAllowedHandler) {
1048
- const result = this.methodNotAllowedHandler(
1049
- ctx,
1050
- allowedMethods,
1051
- );
1052
- if (
1053
- result &&
1054
- typeof (result as Promise<void>).then === 'function'
1055
- ) {
1056
- await result;
1057
- }
1058
- if (!res.writableEnded && !res.headersSent) {
1059
- res.statusCode = 405;
1060
- res.setHeader(
1061
- 'content-type',
1062
- 'text/plain; charset=utf-8',
1063
- );
1064
- res.end('Method Not Allowed');
1065
- }
1066
- } else if (!hasAnyMethod && this.notFoundHandler) {
1067
- const result = this.notFoundHandler(ctx);
1068
- if (
1069
- result &&
1070
- typeof (result as Promise<void>).then === 'function'
1071
- ) {
1072
- await result;
1073
- }
1074
- if (!res.writableEnded && !res.headersSent) {
1075
- res.statusCode = 404;
1076
- res.setHeader(
1077
- 'content-type',
1078
- 'text/plain; charset=utf-8',
1079
- );
1080
- res.end('Not Found');
1081
- }
1082
- } else if (hasAnyMethod) {
1083
- if (!res.headersSent) {
1084
- res.statusCode = 405;
1085
- res.setHeader(
1086
- 'content-type',
1087
- 'text/plain; charset=utf-8',
1088
- );
1089
- }
1090
- res.end('Method Not Allowed');
1091
- } else {
1092
- if (!res.headersSent) {
1093
- res.statusCode = 404;
1094
- res.setHeader(
1095
- 'content-type',
1096
- 'text/plain; charset=utf-8',
1097
- );
1098
- }
1099
- res.end('Not Found');
1100
- }
1101
- } else if (!ctx.disableAutoEnd) {
1102
- res.end();
1103
- }
1104
- }
1105
- } catch (err) {
1106
- await this.handleError(err, ctx);
1107
- }
1108
- };
1109
-
1110
- if (this.ultraMode) {
1111
- await handle();
1112
- } else {
1113
- await this.scheduler.run(handle, {
1114
- priority: match.priority,
1115
- onOverloaded: overloaded,
1116
- timeoutMs: this.options.requestTimeoutMs,
1117
- onTimeout,
1118
- });
1119
- }
1120
-
1121
- const duration = Date.now() - start;
1122
- if (!this.ultraMode) {
1123
- this.metrics.onRequestEnd(duration, res.statusCode);
1124
- if (timedOut) {
1125
- this.metrics.onTimeout();
1126
- }
1127
- }
1128
-
1129
- if (this.tracer) {
1130
- const event = {
1131
- type: 'request_end' as const,
1132
- method,
1133
- path: url.pathname,
1134
- statusCode: res.statusCode,
1135
- durationMs: duration,
1136
- requestId,
1137
- };
1138
- const result = this.tracer(event);
1139
- if (result && typeof (result as Promise<void>).then === 'function') {
1140
- void result;
1141
- }
1142
- }
1143
-
1144
- this.releaseContext(ctx);
1145
- }
1146
-
1147
- private async handleUpgrade(
1148
- req: IncomingMessage,
1149
- socket: Duplex,
1150
- head: Buffer,
1151
- ): Promise<void> {
1152
- await this.wsManager.handleUpgrade(req, socket, head);
1153
- }
1154
-
1155
- private async runLifecycleHooks(
1156
- hooks: Array<() => void | Promise<void>>,
1157
- ): Promise<void> {
1158
- if (hooks.length === 0) {
1159
- return;
1160
- }
1161
- for (const hook of hooks) {
1162
- try {
1163
- const result = hook();
1164
- if (result && typeof (result as Promise<void>).then === 'function') {
1165
- await result;
1166
- }
1167
- } catch {
1168
- // Ignore hook errors to avoid impacting core server flow
1169
- }
1170
- }
1171
- }
1172
-
1173
- private handleError(err: unknown, ctx: QHTTPXContext): Promise<void> | void {
1174
- const res = ctx.res;
1175
- if (res.writableEnded) {
1176
- return;
1177
- }
1178
-
1179
- if (this.errorHandler) {
1180
- try {
1181
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1182
- const errorContext = ctx as any;
1183
- errorContext.error = err;
1184
- const result = this.errorHandler(errorContext);
1185
- if (result && typeof (result as Promise<void>).then === 'function') {
1186
- return (result as Promise<void>).then(() => {
1187
- // Ensure response is sent if handler didn't
1188
- });
1189
- }
1190
- if (res.writableEnded) {
1191
- return;
1192
- }
1193
- } catch (handlerErr) {
1194
- // Fall through to default error handling below
1195
- console.error('Error in error handler:', handlerErr);
1196
- }
1197
- }
1198
-
1199
- if (res.writableEnded) {
1200
- return;
1201
- }
1202
-
1203
- if (err instanceof HttpError) {
1204
- if (!res.headersSent) {
1205
- res.statusCode = err.status;
1206
- res.setHeader('content-type', 'application/json; charset=utf-8');
1207
- }
1208
- const payload = {
1209
- error: {
1210
- message: err.message,
1211
- code: err.code ?? 'HTTP_ERROR',
1212
- details: err.details,
1213
- },
1214
- };
1215
- const serializer = this.options.jsonSerializer;
1216
- const body =
1217
- serializer !== undefined ? serializer(payload) : JSON.stringify(payload);
1218
- res.end(body);
1219
- return;
1220
- }
1221
-
1222
- if (!res.headersSent) {
1223
- res.statusCode = 500;
1224
- res.setHeader('content-type', 'text/plain; charset=utf-8');
1225
- }
1226
- res.end('Internal Server Error');
1227
- }
1228
-
1229
- private generateRequestId(): string {
1230
- const id = this.nextRequestId;
1231
- this.nextRequestId += 1;
1232
- return `${Date.now().toString(36)}-${id.toString(36)}`;
1233
- }
1234
- }