@faapi/faapi 3.0.0 → 3.1.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/dist/testing.js CHANGED
@@ -103,6 +103,144 @@ function createSseWriter() {
103
103
  return writer;
104
104
  }
105
105
 
106
+ // src/errors/FaapiError.ts
107
+ var FaapiError = class extends Error {
108
+ constructor(code, message, statusCode) {
109
+ super(message);
110
+ this.code = code;
111
+ this.statusCode = statusCode;
112
+ this.name = "FaapiError";
113
+ }
114
+ code;
115
+ statusCode;
116
+ };
117
+
118
+ // src/errors/httpErrors.ts
119
+ function deriveStatusCode(issues) {
120
+ const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
121
+ return has400 ? 400 : 422;
122
+ }
123
+ var ValidationError = class extends FaapiError {
124
+ constructor(message, issues) {
125
+ super("VALIDATION_ERROR", message, deriveStatusCode(issues));
126
+ this.issues = issues;
127
+ this.name = "ValidationError";
128
+ }
129
+ issues;
130
+ };
131
+ var RouteNotFoundError = class extends FaapiError {
132
+ constructor(path11) {
133
+ super("ROUTE_NOT_FOUND", `Route not found: ${path11}`, 404);
134
+ this.name = "RouteNotFoundError";
135
+ }
136
+ };
137
+ var MethodNotAllowedError = class extends FaapiError {
138
+ constructor(method, path11, allowedMethods) {
139
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path11}`, 405);
140
+ this.allowedMethods = allowedMethods;
141
+ this.name = "MethodNotAllowedError";
142
+ }
143
+ allowedMethods;
144
+ };
145
+ var InternalError = class extends FaapiError {
146
+ constructor(message) {
147
+ super("INTERNAL_ERROR", message, 500);
148
+ this.name = "InternalError";
149
+ }
150
+ };
151
+ var PayloadTooLargeError = class extends FaapiError {
152
+ constructor(maxSize) {
153
+ super("PAYLOAD_TOO_LARGE", `Request body exceeds size limit of ${maxSize} bytes`, 413);
154
+ this.name = "PayloadTooLargeError";
155
+ }
156
+ };
157
+
158
+ // src/response/responseFormatter.ts
159
+ function defaultOk(data) {
160
+ return { data };
161
+ }
162
+ function defaultFail(e) {
163
+ const error = { message: e.message };
164
+ if (e.code !== void 0) error.code = e.code;
165
+ return { error };
166
+ }
167
+ function getResponseConfig(config) {
168
+ return config?.response;
169
+ }
170
+ function resolveOkFn(config) {
171
+ return getResponseConfig(config)?.ok ?? defaultOk;
172
+ }
173
+ function resolveFailFn(config) {
174
+ return getResponseConfig(config)?.fail ?? defaultFail;
175
+ }
176
+ function jsonOk(body, status = 200, extraHeaders) {
177
+ return jsonRaw(body, status, extraHeaders);
178
+ }
179
+ function jsonRaw(body, status, extraHeaders) {
180
+ const headers = new Headers({ "Content-Type": "application/json" });
181
+ if (extraHeaders) {
182
+ const extra = new Headers(extraHeaders);
183
+ extra.forEach((value, key) => headers.set(key, value));
184
+ }
185
+ return new Response(JSON.stringify(body), { status, headers });
186
+ }
187
+ function wrapOkResult(result, config) {
188
+ if (result instanceof Response) return result;
189
+ return resolveOkFn(config)(result);
190
+ }
191
+ function formatFailResponse(options, config) {
192
+ const failFn = resolveFailFn(config);
193
+ const body = failFn({
194
+ status: options.status,
195
+ code: options.code,
196
+ message: options.message
197
+ });
198
+ return jsonOk(body, options.status ?? 500);
199
+ }
200
+ function formatErrorResponse(error, config) {
201
+ const failFn = resolveFailFn(config);
202
+ if (error instanceof ValidationError) {
203
+ const body2 = failFn({
204
+ status: error.statusCode,
205
+ code: error.code,
206
+ message: error.message
207
+ });
208
+ const bodyObj = typeof body2 === "object" && body2 !== null ? body2 : { error: body2 };
209
+ const errorObj = bodyObj.error ?? bodyObj;
210
+ if (errorObj) {
211
+ errorObj.issues = error.issues;
212
+ }
213
+ return jsonOk(bodyObj, error.statusCode);
214
+ }
215
+ if (error instanceof MethodNotAllowedError) {
216
+ const body2 = failFn({
217
+ status: error.statusCode,
218
+ code: error.code,
219
+ message: error.message
220
+ });
221
+ return jsonOk(body2, error.statusCode, { Allow: error.allowedMethods.join(", ") });
222
+ }
223
+ if (error instanceof PayloadTooLargeError) {
224
+ const body2 = failFn({
225
+ status: error.statusCode,
226
+ code: error.code,
227
+ message: error.message
228
+ });
229
+ return jsonOk(body2, error.statusCode);
230
+ }
231
+ if (error instanceof FaapiError) {
232
+ const body2 = failFn({
233
+ status: error.statusCode,
234
+ code: error.code,
235
+ message: error.message
236
+ });
237
+ return jsonOk(body2, error.statusCode);
238
+ }
239
+ const message = error instanceof Error ? error.message : "An unknown error occurred";
240
+ const body = failFn({ status: 500, code: "INTERNAL_ERROR", message });
241
+ return jsonOk(body, 500);
242
+ }
243
+
106
244
  // src/runtime/createContext.ts
107
245
  function parseCookies(cookieHeader) {
108
246
  const cookies = /* @__PURE__ */ new Map();
@@ -163,11 +301,7 @@ function createContext(request, params, config = {}, ip = "") {
163
301
  });
164
302
  },
165
303
  json(data, status) {
166
- const headers = { "Content-Type": "application/json" };
167
- return new Response(JSON.stringify(data), {
168
- status: status ?? 200,
169
- headers
170
- });
304
+ return jsonOk(data, status ?? 200);
171
305
  },
172
306
  html(html, status) {
173
307
  const headers = { "Content-Type": "text/html; charset=utf-8" };
@@ -203,18 +337,22 @@ function createContext(request, params, config = {}, ip = "") {
203
337
  /**
204
338
  * 显式包装成功响应(返回 Response,不会被自动包裹再次包装)
205
339
  *
340
+ * 实现委托给 [responseFormatter.wrapOkResult](../response/responseFormatter.ts),
341
+ * 与 handler `return data` 走的自动包裹路径共享同一套 ok 函数。
342
+ *
206
343
  * 用 config.response.ok(或默认 (data) => ({ data })) 包裹 data 并返回 JSON Response。
207
344
  * handler 也可直接 return data,框架会自动用 ok 包裹,两者等价。
208
345
  */
209
346
  ok(data) {
210
- const responseConfig = config.response;
211
- const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
212
- const body = okFn(data);
213
- return ctx.json(body);
347
+ const body = wrapOkResult(data, config);
348
+ return jsonOk(body, 200);
214
349
  },
215
350
  /**
216
351
  * 返回错误响应(对象形式参数,status 和 code 均可省略)
217
352
  *
353
+ * 实现委托给 [responseFormatter.formatFailResponse](../response/responseFormatter.ts),
354
+ * 与 formatErrorResponse(handler 抛错兜底)共享同一套 fail 函数,确保错误格式一致。
355
+ *
218
356
  * - status 省略时 HTTP 状态码默认 500
219
357
  * - code 省略时响应 body 里不含 code 字段(默认 fail 函数只放非 undefined 的字段)
220
358
  * - status 和 code 独立无关联
@@ -222,18 +360,7 @@ function createContext(request, params, config = {}, ip = "") {
222
360
  * body 用 config.response.fail(或默认实现)包装。
223
361
  */
224
362
  fail(options) {
225
- const responseConfig = config.response;
226
- const failFn = responseConfig?.fail ?? ((e) => {
227
- const error = { message: e.message };
228
- if (e.code !== void 0) error.code = e.code;
229
- return { error };
230
- });
231
- const body = failFn({
232
- status: options.status,
233
- code: options.code,
234
- message: options.message
235
- });
236
- return ctx.json(body, options.status ?? 500);
363
+ return formatFailResponse(options, config);
237
364
  }
238
365
  };
239
366
  const extend = config?.extendContext;
@@ -463,12 +590,21 @@ function queryToObject(params) {
463
590
  return result;
464
591
  }
465
592
 
466
- // src/injection/agentRegistry.ts
593
+ // src/injection/skillRegistry.ts
467
594
  var registry = /* @__PURE__ */ new Map();
468
- function listAgents() {
595
+ function listSkills() {
469
596
  return Array.from(registry.values());
470
597
  }
471
598
 
599
+ // src/injection/agentRegistry.ts
600
+ var registry2 = /* @__PURE__ */ new Map();
601
+ function listAgents() {
602
+ const merged = /* @__PURE__ */ new Map();
603
+ for (const agent of registry2.values()) merged.set(agent.name, agent);
604
+ for (const skill of listSkills()) merged.set(skill.name, skill);
605
+ return Array.from(merged.values());
606
+ }
607
+
472
608
  // src/injection/agentHandle.ts
473
609
  var currentFactory = null;
474
610
  function getAgentHandle(ctx) {
@@ -540,10 +676,7 @@ async function injectParamsAsync(handler, ctx, body, injectors) {
540
676
 
541
677
  // src/runtime/invokeHandler.ts
542
678
  function wrapResult(result, ctx) {
543
- if (result instanceof Response) return result;
544
- const responseConfig = ctx.config.response;
545
- const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
546
- return okFn(result);
679
+ return wrapOkResult(result, ctx.config);
547
680
  }
548
681
  function mergeMeta(response, meta) {
549
682
  const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
@@ -726,37 +859,42 @@ function setCachedMiddlewares(absPath, bundle) {
726
859
  middlewareCache.set(absPath, bundle);
727
860
  }
728
861
  async function loadMiddlewaresFile(filePath) {
862
+ let module;
729
863
  try {
730
- const module = await importWithCacheBust(filePath);
731
- const middlewares = module.default ?? module.middlewares ?? [];
732
- if (!Array.isArray(middlewares)) {
733
- console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
734
- return { middlewares: [], injectors: {} };
735
- }
736
- const validMiddlewares = middlewares.filter((m) => {
737
- if (typeof m !== "function") {
738
- console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
739
- return false;
740
- }
741
- return true;
742
- });
743
- const injectors = module.injectors ?? {};
744
- if (typeof injectors !== "object" || injectors === null) {
745
- console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
746
- return { middlewares: validMiddlewares, injectors: {} };
747
- }
748
- const validInjectors = {};
749
- for (const [name, injector] of Object.entries(injectors)) {
750
- if (typeof injector !== "function") {
751
- console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
752
- continue;
753
- }
754
- validInjectors[name] = injector;
755
- }
756
- return { middlewares: validMiddlewares, injectors: validInjectors };
757
- } catch {
864
+ module = await importWithCacheBust(filePath);
865
+ } catch (err) {
866
+ console.error(
867
+ `[faapi] Failed to load middlewares from ${filePath}:`,
868
+ err instanceof Error ? err.stack ?? err.message : err
869
+ );
870
+ return { middlewares: [], injectors: {} };
871
+ }
872
+ const middlewares = module.default ?? module.middlewares ?? [];
873
+ if (!Array.isArray(middlewares)) {
874
+ console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
758
875
  return { middlewares: [], injectors: {} };
759
876
  }
877
+ const validMiddlewares = middlewares.filter((m) => {
878
+ if (typeof m !== "function") {
879
+ console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
880
+ return false;
881
+ }
882
+ return true;
883
+ });
884
+ const injectors = module.injectors ?? {};
885
+ if (typeof injectors !== "object" || injectors === null) {
886
+ console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
887
+ return { middlewares: validMiddlewares, injectors: {} };
888
+ }
889
+ const validInjectors = {};
890
+ for (const [name, injector] of Object.entries(injectors)) {
891
+ if (typeof injector !== "function") {
892
+ console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
893
+ continue;
894
+ }
895
+ validInjectors[name] = injector;
896
+ }
897
+ return { middlewares: validMiddlewares, injectors: validInjectors };
760
898
  }
761
899
  async function loadMergedMiddlewares(middlewarePaths) {
762
900
  if (middlewarePaths.length === 0) return void 0;
@@ -2072,52 +2210,6 @@ async function writeSchemaFile(outputPath, source) {
2072
2210
  await fs2.writeFile(outputPath, source, "utf-8");
2073
2211
  }
2074
2212
 
2075
- // src/errors/FaapiError.ts
2076
- var FaapiError = class extends Error {
2077
- constructor(code, message, statusCode) {
2078
- super(message);
2079
- this.code = code;
2080
- this.statusCode = statusCode;
2081
- this.name = "FaapiError";
2082
- }
2083
- code;
2084
- statusCode;
2085
- };
2086
-
2087
- // src/errors/httpErrors.ts
2088
- function deriveStatusCode(issues) {
2089
- const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
2090
- return has400 ? 400 : 422;
2091
- }
2092
- var ValidationError = class extends FaapiError {
2093
- constructor(message, issues) {
2094
- super("VALIDATION_ERROR", message, deriveStatusCode(issues));
2095
- this.issues = issues;
2096
- this.name = "ValidationError";
2097
- }
2098
- issues;
2099
- };
2100
- var RouteNotFoundError = class extends FaapiError {
2101
- constructor(path11) {
2102
- super("ROUTE_NOT_FOUND", `Route not found: ${path11}`, 404);
2103
- this.name = "RouteNotFoundError";
2104
- }
2105
- };
2106
- var MethodNotAllowedError = class extends FaapiError {
2107
- constructor(method, path11, allowedMethods) {
2108
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path11}`, 405);
2109
- this.allowedMethods = allowedMethods;
2110
- this.name = "MethodNotAllowedError";
2111
- }
2112
- allowedMethods;
2113
- };
2114
- var InternalError = class extends FaapiError {
2115
- constructor(message) {
2116
- super("INTERNAL_ERROR", message, 500);
2117
- this.name = "InternalError";
2118
- }
2119
- };
2120
-
2121
2213
  // src/cli/compileOnDemand.ts
2122
2214
  import path7 from "path";
2123
2215
  import fs6 from "fs";
@@ -2371,9 +2463,25 @@ function isProductFresh(sourceAbsPath, productAbsPath) {
2371
2463
  return false;
2372
2464
  }
2373
2465
  }
2374
- var compiledFiles = /* @__PURE__ */ new Set();
2466
+ function createDevOnDemandState() {
2467
+ return {
2468
+ enabled: false,
2469
+ distDir: void 0,
2470
+ compiledFiles: /* @__PURE__ */ new Set(),
2471
+ generatedSchemas: /* @__PURE__ */ new Set(),
2472
+ inFlightCompilations: /* @__PURE__ */ new Map(),
2473
+ inFlightSchemaGenerations: /* @__PURE__ */ new Map()
2474
+ };
2475
+ }
2476
+ var state = createDevOnDemandState();
2375
2477
  async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2376
- if (compiledFiles.has(sourceAbsPath)) {
2478
+ const inFlight = state.inFlightCompilations.get(sourceAbsPath);
2479
+ if (inFlight) {
2480
+ await inFlight.catch(() => {
2481
+ });
2482
+ return false;
2483
+ }
2484
+ if (state.compiledFiles.has(sourceAbsPath)) {
2377
2485
  return false;
2378
2486
  }
2379
2487
  if (!fs6.existsSync(sourceAbsPath)) {
@@ -2381,17 +2489,25 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2381
2489
  }
2382
2490
  const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
2383
2491
  if (productPath && isProductFresh(sourceAbsPath, productPath)) {
2384
- compiledFiles.add(sourceAbsPath);
2492
+ state.compiledFiles.add(sourceAbsPath);
2385
2493
  return false;
2386
2494
  }
2387
- await compileDevRoutes({
2388
- rootDir,
2389
- dist,
2390
- files: [sourceAbsPath],
2391
- logLevel: "silent"
2392
- });
2393
- compiledFiles.add(sourceAbsPath);
2394
- return true;
2495
+ const compilePromise = (async () => {
2496
+ await compileDevRoutes({
2497
+ rootDir,
2498
+ dist,
2499
+ files: [sourceAbsPath],
2500
+ logLevel: "silent"
2501
+ });
2502
+ state.compiledFiles.add(sourceAbsPath);
2503
+ })();
2504
+ state.inFlightCompilations.set(sourceAbsPath, compilePromise);
2505
+ try {
2506
+ await compilePromise;
2507
+ return true;
2508
+ } finally {
2509
+ state.inFlightCompilations.delete(sourceAbsPath);
2510
+ }
2395
2511
  }
2396
2512
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2397
2513
  const rel = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
@@ -2400,9 +2516,14 @@ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2400
2516
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2401
2517
  return path7.resolve(rootDir, dist, jsRel);
2402
2518
  }
2403
- var generatedSchemas = /* @__PURE__ */ new Set();
2404
2519
  async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2405
- if (generatedSchemas.has(schemaPath)) {
2520
+ const inFlight = state.inFlightSchemaGenerations.get(schemaPath);
2521
+ if (inFlight) {
2522
+ await inFlight.catch(() => {
2523
+ });
2524
+ return false;
2525
+ }
2526
+ if (state.generatedSchemas.has(schemaPath)) {
2406
2527
  return false;
2407
2528
  }
2408
2529
  const prodAbsPath = path7.resolve(rootDir, routeFilePath);
@@ -2411,7 +2532,7 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2411
2532
  return false;
2412
2533
  }
2413
2534
  if (isProductFresh(sourceAbsPath, schemaPath)) {
2414
- generatedSchemas.add(schemaPath);
2535
+ state.generatedSchemas.add(schemaPath);
2415
2536
  return false;
2416
2537
  }
2417
2538
  const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
@@ -2420,9 +2541,17 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2420
2541
  }
2421
2542
  const sourceRelPath = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2422
2543
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2423
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
2424
- generatedSchemas.add(schemaPath);
2425
- return true;
2544
+ const generatePromise = (async () => {
2545
+ await generateSchemaFiles(sourceRoutes, rootDir, dist);
2546
+ state.generatedSchemas.add(schemaPath);
2547
+ })();
2548
+ state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
2549
+ try {
2550
+ await generatePromise;
2551
+ return true;
2552
+ } finally {
2553
+ state.inFlightSchemaGenerations.delete(schemaPath);
2554
+ }
2426
2555
  }
2427
2556
  function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2428
2557
  const rel = path7.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
@@ -2436,13 +2565,11 @@ function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2436
2565
  if (fs6.existsSync(tsAbs)) return tsAbs;
2437
2566
  return path7.resolve(rootDir, srcRel);
2438
2567
  }
2439
- var devOnDemandEnabled = false;
2440
2568
  function isDevOnDemandEnabled() {
2441
- return devOnDemandEnabled;
2569
+ return state.enabled;
2442
2570
  }
2443
- var devDistDir;
2444
2571
  function getDevDist() {
2445
- return devDistDir;
2572
+ return state.distDir;
2446
2573
  }
2447
2574
 
2448
2575
  // src/validator/validateInput.ts
@@ -2965,52 +3092,6 @@ import fs8 from "fs";
2965
3092
  import { WebSocketServer, WebSocket } from "ws";
2966
3093
  import path8 from "path";
2967
3094
 
2968
- // src/errors/formatErrorResponse.ts
2969
- function formatErrorResponse(error) {
2970
- if (error instanceof ValidationError) {
2971
- const body2 = {
2972
- code: error.code,
2973
- message: error.message,
2974
- issues: error.issues
2975
- };
2976
- return new Response(JSON.stringify({ error: body2 }), {
2977
- status: error.statusCode,
2978
- headers: { "Content-Type": "application/json" }
2979
- });
2980
- }
2981
- if (error instanceof MethodNotAllowedError) {
2982
- const body2 = {
2983
- code: error.code,
2984
- message: error.message
2985
- };
2986
- return new Response(JSON.stringify({ error: body2 }), {
2987
- status: error.statusCode,
2988
- headers: {
2989
- "Content-Type": "application/json",
2990
- Allow: error.allowedMethods.join(", ")
2991
- }
2992
- });
2993
- }
2994
- if (error instanceof FaapiError) {
2995
- const body2 = {
2996
- code: error.code,
2997
- message: error.message
2998
- };
2999
- return new Response(JSON.stringify({ error: body2 }), {
3000
- status: error.statusCode,
3001
- headers: { "Content-Type": "application/json" }
3002
- });
3003
- }
3004
- const body = {
3005
- code: "INTERNAL_ERROR",
3006
- message: error instanceof Error ? error.message : "An unknown error occurred"
3007
- };
3008
- return new Response(JSON.stringify({ error: body }), {
3009
- status: 500,
3010
- headers: { "Content-Type": "application/json" }
3011
- });
3012
- }
3013
-
3014
3095
  // src/server/serverUtils.ts
3015
3096
  function nodeHttpToWebHeaders(req) {
3016
3097
  const headers = new Headers();
@@ -3024,9 +3105,9 @@ function nodeHttpToWebHeaders(req) {
3024
3105
  }
3025
3106
  return headers;
3026
3107
  }
3027
- function buildErrorResponse(err) {
3108
+ function buildErrorResponse(err, config) {
3028
3109
  try {
3029
- return formatErrorResponse(err);
3110
+ return formatErrorResponse(err, config);
3030
3111
  } catch {
3031
3112
  return new Response(
3032
3113
  JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
@@ -3221,25 +3302,51 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
3221
3302
  }
3222
3303
  function limitStreamSize(stream, maxSize) {
3223
3304
  let totalSize = 0;
3224
- const reader = stream.getReader();
3225
- return new ReadableStream({
3226
- async pull(controller) {
3227
- const { done, value } = await reader.read();
3228
- if (done) {
3229
- controller.close();
3305
+ let reader;
3306
+ let errored = false;
3307
+ const releaseReader = () => {
3308
+ if (reader) {
3309
+ try {
3230
3310
  reader.releaseLock();
3231
- return;
3311
+ } catch {
3232
3312
  }
3233
- totalSize += value.byteLength;
3234
- if (totalSize > maxSize) {
3235
- controller.error(new Error(`\u8BF7\u6C42\u4F53\u8D85\u8FC7\u5927\u5C0F\u9650\u5236 ${maxSize} \u5B57\u8282`));
3236
- reader.releaseLock();
3237
- return;
3313
+ reader = void 0;
3314
+ }
3315
+ };
3316
+ const failStream = (controller, err) => {
3317
+ if (errored) return;
3318
+ errored = true;
3319
+ controller.error(err instanceof Error ? err : new Error(String(err)));
3320
+ releaseReader();
3321
+ };
3322
+ return new ReadableStream({
3323
+ async pull(controller) {
3324
+ if (!reader) reader = stream.getReader();
3325
+ try {
3326
+ const { done, value } = await reader.read();
3327
+ if (done) {
3328
+ controller.close();
3329
+ releaseReader();
3330
+ return;
3331
+ }
3332
+ totalSize += value.byteLength;
3333
+ if (totalSize > maxSize) {
3334
+ failStream(controller, new PayloadTooLargeError(maxSize));
3335
+ return;
3336
+ }
3337
+ controller.enqueue(value);
3338
+ } catch (err) {
3339
+ failStream(controller, err);
3238
3340
  }
3239
- controller.enqueue(value);
3240
3341
  },
3241
3342
  cancel(reason) {
3242
- reader.cancel(reason);
3343
+ if (reader) {
3344
+ try {
3345
+ reader.cancel(reason);
3346
+ } catch {
3347
+ }
3348
+ releaseReader();
3349
+ }
3243
3350
  }
3244
3351
  });
3245
3352
  }
@@ -3320,21 +3427,27 @@ function createServer(options) {
3320
3427
  }
3321
3428
  return { server, routesRef };
3322
3429
  }
3323
- async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
3430
+ function prepareRequest(req, config, bodyLimit) {
3324
3431
  const request = toWebRequest(req, bodyLimit);
3325
3432
  const method = request.method.toUpperCase();
3326
3433
  const urlPath = new URL(request.url).pathname;
3327
3434
  const ctx = createContext(request, {}, config, getClientIp(req));
3328
3435
  const meta = ctx.meta;
3329
- const routePipeline = async () => {
3330
- const match = matchRoute(routes, method, urlPath);
3331
- if (!match) {
3332
- const allowedMethods = findAllowedMethods(routes, urlPath);
3333
- if (allowedMethods.length > 0) {
3334
- throw new MethodNotAllowedError(method, urlPath, allowedMethods);
3335
- }
3336
- throw new RouteNotFoundError(urlPath);
3337
- }
3436
+ return { request, ctx, meta, method, urlPath };
3437
+ }
3438
+ function resolveRouteOrThrow(routes, method, urlPath) {
3439
+ const match = matchRoute(routes, method, urlPath);
3440
+ if (match) return match;
3441
+ const allowedMethods = findAllowedMethods(routes, urlPath);
3442
+ if (allowedMethods.length > 0) {
3443
+ throw new MethodNotAllowedError(method, urlPath, allowedMethods);
3444
+ }
3445
+ throw new RouteNotFoundError(urlPath);
3446
+ }
3447
+ function createRoutePipeline(opts) {
3448
+ const { routes, method, urlPath, ctx, request, rootDir, dist, globalInjectors } = opts;
3449
+ return async () => {
3450
+ const match = resolveRouteOrThrow(routes, method, urlPath);
3338
3451
  ctx.params = match.params;
3339
3452
  const { route } = match;
3340
3453
  const absoluteFilePath = path9.resolve(rootDir, route.filePath);
@@ -3364,37 +3477,44 @@ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares,
3364
3477
  }
3365
3478
  }
3366
3479
  const mergedInjectors = globalInjectors ? { ...globalInjectors, ...route.injectors } : route.injectors;
3367
- const response = await invokeHandler(
3368
- routeModule.handler,
3369
- ctx,
3370
- body,
3371
- route.middlewares,
3372
- mergedInjectors
3373
- );
3374
- return response;
3480
+ return await invokeHandler(routeModule.handler, ctx, body, route.middlewares, mergedInjectors);
3375
3481
  };
3376
- try {
3377
- let response;
3378
- const outerMiddlewares = [];
3379
- if (configMiddlewares.length > 0) outerMiddlewares.push(...configMiddlewares);
3380
- if (globalMiddlewares && globalMiddlewares.length > 0) {
3381
- outerMiddlewares.push(...globalMiddlewares);
3382
- }
3383
- if (outerMiddlewares.length > 0) {
3384
- response = await compose(outerMiddlewares, ctx, routePipeline);
3385
- } else {
3386
- response = await routePipeline();
3482
+ }
3483
+ async function sendSuccessResponse(response, res) {
3484
+ await sendNodeResponse(response, res);
3485
+ }
3486
+ async function sendErrorResponse(err, meta, res, onError, ctx) {
3487
+ await sendNodeResponse(mergeMeta(buildErrorResponse(err, ctx.config), meta), res);
3488
+ if (onError) {
3489
+ try {
3490
+ await onError(err, ctx);
3491
+ } catch {
3387
3492
  }
3388
- await sendNodeResponse(response, res);
3493
+ }
3494
+ }
3495
+ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
3496
+ const { request, ctx, meta, method, urlPath } = prepareRequest(req, config, bodyLimit);
3497
+ const routePipeline = createRoutePipeline({
3498
+ routes,
3499
+ method,
3500
+ urlPath,
3501
+ ctx,
3502
+ request,
3503
+ rootDir,
3504
+ dist,
3505
+ globalMiddlewares,
3506
+ globalInjectors
3507
+ });
3508
+ const outerMiddlewares = [];
3509
+ if (configMiddlewares.length > 0) outerMiddlewares.push(...configMiddlewares);
3510
+ if (globalMiddlewares && globalMiddlewares.length > 0) {
3511
+ outerMiddlewares.push(...globalMiddlewares);
3512
+ }
3513
+ try {
3514
+ const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
3515
+ await sendSuccessResponse(response, res);
3389
3516
  } catch (err) {
3390
- const errorResponse = buildErrorResponse(err);
3391
- await sendNodeResponse(mergeMeta(errorResponse, meta), res);
3392
- if (onError) {
3393
- try {
3394
- await onError(err, ctx);
3395
- } catch {
3396
- }
3397
- }
3517
+ await sendErrorResponse(err, meta, res, onError, ctx);
3398
3518
  }
3399
3519
  }
3400
3520