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