koatty_router 2.1.10 → 2.2.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/index.mjs CHANGED
@@ -10,7 +10,7 @@ import { buildSchema } from 'koatty_graphql';
10
10
  import 'reflect-metadata';
11
11
  import { OnEvent, AppEvent, Component, CONTROLLER_ROUTER, KoattyApplication } from 'koatty_core';
12
12
  import { Project } from 'ts-morph';
13
- import { ClassValidator, plainToClass, convertParamsType, PARAM_RULE_KEY, PARAM_CHECK_KEY, PARAM_TYPE_KEY, FunctionValidator, paramterTypes } from 'koatty_validation';
13
+ import { ClassValidator, plainToClass, PARAM_RULE_KEY, PARAM_CHECK_KEY, PARAM_TYPE_KEY, FunctionValidator, convertParamsType, paramterTypes } from 'koatty_validation';
14
14
  import compose from 'koa-compose';
15
15
  import { LRUCache } from 'lru-cache';
16
16
  import getRawBody from 'raw-body';
@@ -24,7 +24,7 @@ import { LoadProto, ListServices } from 'koatty_proto';
24
24
 
25
25
  /*!
26
26
  * @Author: richen
27
- * @Date: 2026-03-08 04:45:07
27
+ * @Date: 2026-04-24 08:20:32
28
28
  * @License: BSD (3-Clause)
29
29
  * @Copyright (c) - <richenlin(at)gmail.com>
30
30
  * @HomePage: https://koatty.org/
@@ -759,6 +759,9 @@ function RegisterMiddleware(app, config) {
759
759
  };
760
760
  }
761
761
  __name(RegisterMiddleware, "RegisterMiddleware");
762
+
763
+ // src/payload/interface.ts
764
+ var FILE_KEY = /* @__PURE__ */ Symbol.for("koatty.files");
762
765
  function parseText(ctx, opts) {
763
766
  return getRawBody(inflate(ctx.req), opts).catch((err) => {
764
767
  DefaultLogger.Error(err);
@@ -770,8 +773,9 @@ async function parseJson(ctx, opts) {
770
773
  const str = await parseText(ctx, opts);
771
774
  if (!str) return {};
772
775
  try {
773
- return {
774
- body: JSON.parse(str)
776
+ const parsed = JSON.parse(str);
777
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {
778
+ value: parsed
775
779
  };
776
780
  } catch (error) {
777
781
  DefaultLogger.Error(error);
@@ -780,18 +784,13 @@ async function parseJson(ctx, opts) {
780
784
  }
781
785
  __name(parseJson, "parseJson");
782
786
  async function parseForm(ctx, opts) {
783
- if (!ctx.request.headers["content-length"] || !ctx.request.headers["content-type"]?.includes("application/x-www-form-urlencoded")) {
784
- return {};
785
- }
786
787
  const str = await parseText(ctx, opts);
787
788
  if (!str || str.trim().length === 0) {
788
789
  return {};
789
790
  }
790
791
  try {
791
792
  const result = parse(str);
792
- return {
793
- body: result
794
- };
793
+ return result;
795
794
  } catch (error) {
796
795
  DefaultLogger.Error("[FormParseError]", error);
797
796
  return {};
@@ -823,12 +822,6 @@ __name(deleteFiles, "deleteFiles");
823
822
 
824
823
  // src/payload/parser/multipart.ts
825
824
  function parseMultipart(ctx, opts) {
826
- if (!ctx.request.headers["content-type"]?.includes("multipart/form-data")) {
827
- return Promise.resolve({
828
- body: {},
829
- file: {}
830
- });
831
- }
832
825
  const form = new IncomingForm({
833
826
  encoding: opts.encoding,
834
827
  multiples: opts.multiples,
@@ -851,15 +844,12 @@ function parseMultipart(ctx, opts) {
851
844
  if (err) {
852
845
  cleanup();
853
846
  DefaultLogger.Error("[MultipartParseError]", err);
854
- return resolve({
855
- body: {},
856
- file: {}
857
- });
847
+ return resolve({});
858
848
  }
859
849
  uploadFiles = files;
860
850
  resolve({
861
- body: fields,
862
- file: files
851
+ ...fields,
852
+ [FILE_KEY]: files
863
853
  });
864
854
  });
865
855
  });
@@ -873,8 +863,9 @@ async function parseXml(ctx, opts) {
873
863
  const str = await parseText(ctx, opts);
874
864
  if (!str) return {};
875
865
  try {
876
- return {
877
- body: xmlParser.parse(str)
866
+ const parsed = xmlParser.parse(str);
867
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {
868
+ value: parsed
878
869
  };
879
870
  } catch (error) {
880
871
  DefaultLogger.Error(error);
@@ -915,11 +906,14 @@ async function parseWebSocket(ctx, opts) {
915
906
  const str = await parseText(ctx, opts);
916
907
  if (!str) return {};
917
908
  try {
918
- return {
919
- body: JSON.parse(str)
909
+ const parsed = JSON.parse(str);
910
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {
911
+ value: parsed
920
912
  };
921
913
  } catch {
922
- return str;
914
+ return {
915
+ value: str
916
+ };
923
917
  }
924
918
  } catch (error) {
925
919
  DefaultLogger.Error("[WebSocketParseError]", error);
@@ -1139,6 +1133,7 @@ var PayloadCacheManager = class _PayloadCacheManager {
1139
1133
  var cacheManager = PayloadCacheManager.getInstance();
1140
1134
 
1141
1135
  // src/payload/payload.ts
1136
+ var bodyCache = /* @__PURE__ */ new WeakMap();
1142
1137
  var supportedMethods = /* @__PURE__ */ new Set([
1143
1138
  "POST",
1144
1139
  "PUT",
@@ -1196,7 +1191,7 @@ function payload(options) {
1196
1191
  if (!Object.prototype.hasOwnProperty.call(ctx, "requestFile")) {
1197
1192
  Helper.define(ctx, "requestFile", async (name) => {
1198
1193
  const body = await bodyParser(ctx, opts);
1199
- const files = body.file ?? {};
1194
+ const files = body[FILE_KEY] ?? {};
1200
1195
  return name ? files[name] : files;
1201
1196
  });
1202
1197
  }
@@ -1216,22 +1211,32 @@ function queryParser(ctx, _options) {
1216
1211
  return Object.assign({}, query, params);
1217
1212
  }
1218
1213
  __name(queryParser, "queryParser");
1219
- async function bodyParser(ctx, options) {
1214
+ function bodyParser(ctx, options) {
1215
+ const cached = bodyCache.get(ctx);
1216
+ if (cached !== void 0) return cached;
1217
+ return parseBodyAndCache(ctx, options);
1218
+ }
1219
+ __name(bodyParser, "bodyParser");
1220
+ async function parseBodyAndCache(ctx, options) {
1220
1221
  try {
1221
- let body = ctx.getMetaData("_body")[0];
1222
- if (!Helper.isEmpty(body)) {
1223
- return body;
1224
- }
1222
+ const cached = bodyCache.get(ctx);
1223
+ if (cached !== void 0) return cached;
1225
1224
  const opts = cacheManager.getMergedOptions(options);
1226
- body = await parseBody(ctx, opts);
1227
- ctx.setMetaData("_body", body);
1225
+ const body = await parseBody(ctx, opts);
1226
+ bodyCache.set(ctx, body);
1227
+ try {
1228
+ ctx.setMetaData("_body", body);
1229
+ } catch {
1230
+ }
1228
1231
  return body;
1229
1232
  } catch (err) {
1230
1233
  DefaultLogger.Error(err);
1231
- return {};
1234
+ const empty = {};
1235
+ bodyCache.set(ctx, empty);
1236
+ return empty;
1232
1237
  }
1233
1238
  }
1234
- __name(bodyParser, "bodyParser");
1239
+ __name(parseBodyAndCache, "parseBodyAndCache");
1235
1240
  function parseBody(ctx, options) {
1236
1241
  if (!isSupportedMethod(ctx.method)) {
1237
1242
  return Promise.resolve({});
@@ -1465,27 +1470,6 @@ function injectParamMetaData(app, target, options) {
1465
1470
  if (!v.sourceType) {
1466
1471
  v.sourceType = "custom";
1467
1472
  }
1468
- switch (v.sourceType) {
1469
- case "query":
1470
- v.extractorType = "query";
1471
- break;
1472
- case "body":
1473
- v.extractorType = "body";
1474
- break;
1475
- case "header":
1476
- v.extractorType = "header";
1477
- break;
1478
- case "path":
1479
- v.extractorType = "path";
1480
- break;
1481
- case "file":
1482
- v.extractorType = "file";
1483
- break;
1484
- case "custom":
1485
- v.extractorType = "custom";
1486
- break;
1487
- }
1488
- DefaultLogger.Debug(`Set extractorType ${v.extractorType} for param ${v.name}`);
1489
1473
  const validEntry = validData.find((it) => v.index === it.index && it.name === v.name);
1490
1474
  if (validEntry) {
1491
1475
  v.validRule = validEntry.rule;
@@ -1559,15 +1543,6 @@ function injectParamMetaData(app, target, options) {
1559
1543
  }
1560
1544
  }
1561
1545
  });
1562
- const fastPathScenario = detectFastPathScenario(data);
1563
- if (fastPathScenario) {
1564
- const fastPathHandler = createFastPathHandler(fastPathScenario, data);
1565
- if (fastPathHandler) {
1566
- DefaultLogger.Debug(`Created fast path handler for ${target.constructor.name}.${meta}, scenario: ${fastPathScenario}`);
1567
- data.fastPathHandler = fastPathHandler;
1568
- data.fastPathScenario = fastPathScenario;
1569
- }
1570
- }
1571
1546
  const hasAsyncParams = data.some(
1572
1547
  (p) => p.sourceType === "body" || p.sourceType === "file" || p.isDto
1573
1548
  // DTO validation might be async
@@ -1633,104 +1608,6 @@ function getControllerPath(className) {
1633
1608
  return process.env.APP_PATH + "/controller/" + className + ".ts";
1634
1609
  }
1635
1610
  __name(getControllerPath, "getControllerPath");
1636
- function detectFastPathScenario(params) {
1637
- if (!params || params.length === 0) {
1638
- return null;
1639
- }
1640
- if (params.length === 1) {
1641
- const param = params[0];
1642
- const fnName = param.fn?.name || "";
1643
- if (fnName === "Get" && !param.validRule && !param.isDto) {
1644
- DefaultLogger.Debug(`Detected fast path scenario A: single query param without validation`);
1645
- return "SINGLE_QUERY_NO_VALIDATION";
1646
- }
1647
- if ((fnName === "Post" || fnName === "RequestBody") && param.isDto) {
1648
- DefaultLogger.Debug(`Detected fast path scenario B: single DTO from body`);
1649
- return "SINGLE_DTO_FROM_BODY";
1650
- }
1651
- }
1652
- if (params.length > 1) {
1653
- const allQueryNoValidation = params.every((param) => {
1654
- const fnName = param.fn?.name || "";
1655
- return fnName === "Get" && !param.validRule && !param.isDto;
1656
- });
1657
- if (allQueryNoValidation) {
1658
- DefaultLogger.Debug(`Detected fast path scenario C: multiple query params without validation`);
1659
- return "MULTIPLE_QUERY_NO_VALIDATION";
1660
- }
1661
- }
1662
- return null;
1663
- }
1664
- __name(detectFastPathScenario, "detectFastPathScenario");
1665
- function createFastPathHandler(scenario, params) {
1666
- switch (scenario) {
1667
- case "SINGLE_QUERY_NO_VALIDATION": {
1668
- const param = params[0];
1669
- const paramName = param.name;
1670
- const paramType = param.type;
1671
- if (paramName) {
1672
- return (ctx) => {
1673
- const value = ctx.query?.[paramName];
1674
- const converted = convertParamsType(value, paramType);
1675
- return [
1676
- converted
1677
- ];
1678
- };
1679
- } else {
1680
- return (ctx) => {
1681
- const query = ctx.query || {};
1682
- return [
1683
- query
1684
- ];
1685
- };
1686
- }
1687
- }
1688
- case "SINGLE_DTO_FROM_BODY": {
1689
- const param = params[0];
1690
- const clazz = param.clazz;
1691
- const dtoCheck = param.dtoCheck;
1692
- if (!clazz) {
1693
- DefaultLogger.Debug(`Cannot create fast path: DTO class not found`);
1694
- return null;
1695
- }
1696
- return async (ctx) => {
1697
- const body = await bodyParser(ctx, param.options);
1698
- let validatedValue;
1699
- if (dtoCheck) {
1700
- validatedValue = await ClassValidator.valid(clazz, body, true);
1701
- } else {
1702
- validatedValue = plainToClass(clazz, body, true);
1703
- }
1704
- return [
1705
- validatedValue
1706
- ];
1707
- };
1708
- }
1709
- case "MULTIPLE_QUERY_NO_VALIDATION": {
1710
- const paramConfigs = params.map((p) => ({
1711
- name: p.name,
1712
- type: p.type
1713
- }));
1714
- return (ctx) => {
1715
- const query = ctx.query || {};
1716
- const results = [];
1717
- for (const config of paramConfigs) {
1718
- if (config.name) {
1719
- const value = query[config.name];
1720
- const converted = convertParamsType(value, config.type);
1721
- results.push(converted);
1722
- } else {
1723
- results.push(query);
1724
- }
1725
- }
1726
- return results;
1727
- };
1728
- }
1729
- default:
1730
- return null;
1731
- }
1732
- }
1733
- __name(createFastPathHandler, "createFastPathHandler");
1734
1611
  function compileTypeConverter(type) {
1735
1612
  const normalizedType = type.toLowerCase();
1736
1613
  if (normalizedType === "string") {
@@ -1739,53 +1616,24 @@ function compileTypeConverter(type) {
1739
1616
  switch (normalizedType) {
1740
1617
  case "number":
1741
1618
  return (value) => {
1619
+ if (typeof value === "number") return value;
1742
1620
  if (value === null || value === void 0 || value === "") return value;
1743
- const num = Number(value);
1744
- return isNaN(num) ? value : num;
1621
+ return convertParamsType(value, "number");
1745
1622
  };
1746
1623
  case "boolean":
1747
1624
  return (value) => {
1748
- if (value === null || value === void 0) return value;
1749
1625
  if (typeof value === "boolean") return value;
1750
- if (typeof value === "string") {
1751
- const lower = value.toLowerCase();
1752
- if (lower === "true" || lower === "1") return true;
1753
- if (lower === "false" || lower === "0") return false;
1754
- }
1755
- return Boolean(value);
1626
+ return convertParamsType(value, "boolean");
1756
1627
  };
1757
1628
  case "array":
1758
1629
  return (value) => {
1759
- if (value === null || value === void 0) return value;
1760
1630
  if (Array.isArray(value)) return value;
1761
- if (typeof value === "string") {
1762
- try {
1763
- const parsed = JSON.parse(value);
1764
- return Array.isArray(parsed) ? parsed : [
1765
- value
1766
- ];
1767
- } catch {
1768
- return [
1769
- value
1770
- ];
1771
- }
1772
- }
1773
- return [
1774
- value
1775
- ];
1631
+ return convertParamsType(value, "array");
1776
1632
  };
1777
1633
  case "object":
1778
1634
  return (value) => {
1779
- if (value === null || value === void 0) return value;
1780
- if (typeof value === "object") return value;
1781
- if (typeof value === "string") {
1782
- try {
1783
- return JSON.parse(value);
1784
- } catch {
1785
- return value;
1786
- }
1787
- }
1788
- return value;
1635
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
1636
+ return convertParamsType(value, "object");
1789
1637
  };
1790
1638
  default:
1791
1639
  return (value) => convertParamsType(value, type);
@@ -1813,7 +1661,22 @@ function generatePrecompiledExtractor(param) {
1813
1661
  } else {
1814
1662
  return (ctx) => ctx.params || {};
1815
1663
  }
1816
- case "body":
1664
+ case "body": {
1665
+ const bodyOpts = param.options;
1666
+ if (paramName !== void 0) {
1667
+ return async (ctx) => {
1668
+ const parsed = await bodyParser(ctx, bodyOpts);
1669
+ return (parsed ?? {})[paramName];
1670
+ };
1671
+ } else {
1672
+ return async (ctx) => {
1673
+ const parsed = await bodyParser(ctx, bodyOpts);
1674
+ if (!parsed) return {};
1675
+ const { [FILE_KEY]: _files, ...bodyOnly } = parsed;
1676
+ return bodyOnly;
1677
+ };
1678
+ }
1679
+ }
1817
1680
  case "file":
1818
1681
  return null;
1819
1682
  case "custom":
@@ -1876,14 +1739,19 @@ function extractValueFromSource(source, param) {
1876
1739
  switch (param.sourceType) {
1877
1740
  case ParamSourceType.QUERY:
1878
1741
  return paramName ? source.query?.[paramName] : source.query;
1879
- case ParamSourceType.BODY:
1880
- return paramName ? source.body?.[paramName] : source.body;
1742
+ case ParamSourceType.BODY: {
1743
+ if (paramName) return source.body?.[paramName];
1744
+ const { [FILE_KEY]: _files, ...bodyFields } = source.body || {};
1745
+ return bodyFields;
1746
+ }
1881
1747
  case ParamSourceType.HEADER:
1882
1748
  return paramName ? source.headers?.[paramName] : source.headers;
1883
1749
  case ParamSourceType.PATH:
1884
1750
  return paramName ? source.params?.[paramName] : source.params;
1885
- case ParamSourceType.FILE:
1886
- return paramName ? source.body?.file?.[paramName] : source.body?.file;
1751
+ case ParamSourceType.FILE: {
1752
+ const files = source.body?.[FILE_KEY] || {};
1753
+ return paramName ? files[paramName] : files;
1754
+ }
1887
1755
  case ParamSourceType.CUSTOM:
1888
1756
  return null;
1889
1757
  default:
@@ -1895,21 +1763,18 @@ async function extractParamSources(ctx, params) {
1895
1763
  const needsBody = params.some((param) => {
1896
1764
  return param.sourceType === ParamSourceType.BODY || param.sourceType === ParamSourceType.FILE;
1897
1765
  });
1898
- const bodyData = {};
1766
+ let body = {};
1899
1767
  if (needsBody) {
1900
1768
  try {
1901
1769
  const parsedBody = await bodyParser(ctx, params[0]?.options);
1902
- bodyData.body = parsedBody;
1903
- if (typeof parsedBody === "object" && "file" in parsedBody) {
1904
- bodyData.file = parsedBody.file;
1905
- }
1770
+ body = parsedBody || {};
1906
1771
  } catch (err) {
1907
1772
  DefaultLogger.Error(`extractParamSources: Failed to parse body: ${err.message}`);
1908
1773
  }
1909
1774
  }
1910
1775
  return {
1911
1776
  query: ctx.query || {},
1912
- body: bodyData,
1777
+ body,
1913
1778
  params: ctx.params || {},
1914
1779
  headers: ctx.headers || {}
1915
1780
  };
@@ -1939,7 +1804,7 @@ async function validateParam(app, ctx, value, opt, compiledValidator, compiledTy
1939
1804
  }
1940
1805
  return validatedValue;
1941
1806
  } else {
1942
- const needsConversion = compiledTypeConverter !== null;
1807
+ const needsConversion = compiledTypeConverter != null;
1943
1808
  const needsValidation = !!(compiledValidator || opt.validRule);
1944
1809
  if (!needsConversion && !needsValidation) {
1945
1810
  return value;
@@ -1947,8 +1812,6 @@ async function validateParam(app, ctx, value, opt, compiledValidator, compiledTy
1947
1812
  let convertedValue = value;
1948
1813
  if (compiledTypeConverter) {
1949
1814
  convertedValue = compiledTypeConverter(value);
1950
- } else if (opt.type && opt.type !== "string") {
1951
- convertedValue = convertParamsType(value, opt.type);
1952
1815
  }
1953
1816
  if (compiledValidator) {
1954
1817
  compiledValidator(convertedValue);
@@ -2253,17 +2116,27 @@ var StrategyHandlerFactory = class {
2253
2116
  return converted;
2254
2117
  };
2255
2118
  });
2119
+ const allAsyncHaveExtractor = asyncParams.every((p) => !!p.precompiledExtractor || p.fn && typeof p.fn === "function");
2256
2120
  return async (ctx) => {
2257
- const bodyData = await extractParamSources(ctx, params);
2258
- const asyncResults = asyncParams.map((p) => {
2259
- const rawValue = extractValueFromSource(bodyData, p);
2260
- if (rawValue === null && p.fn) {
2261
- return p.fn(ctx, p.options);
2262
- }
2121
+ const bodyData = allAsyncHaveExtractor ? null : await extractParamSources(ctx, params);
2122
+ const asyncResults = asyncParams.map(async (p) => {
2123
+ if (p.precompiledExtractor) {
2124
+ const rawValue2 = await p.precompiledExtractor(ctx);
2125
+ const value = rawValue2 === void 0 && p.defaultValue !== void 0 ? p.defaultValue : rawValue2;
2126
+ const paramOptions2 = p.precompiledOptions || createParamOptions(p, p.index ?? 0);
2127
+ return validateParam(app, ctx, value, paramOptions2, p.compiledValidator, p.compiledTypeConverter);
2128
+ }
2129
+ if (p.fn && typeof p.fn === "function") {
2130
+ const rawValue2 = await Promise.resolve(p.fn(ctx, p.options));
2131
+ const value = rawValue2 === void 0 && p.defaultValue !== void 0 ? p.defaultValue : rawValue2;
2132
+ const paramOptions2 = p.precompiledOptions || createParamOptions(p, p.index ?? 0);
2133
+ return validateParam(app, ctx, value, paramOptions2, p.compiledValidator, p.compiledTypeConverter);
2134
+ }
2135
+ const rawValue = bodyData ? extractValueFromSource(bodyData, p) : void 0;
2263
2136
  if (rawValue === void 0 && p.defaultValue !== void 0) {
2264
2137
  return p.defaultValue;
2265
2138
  }
2266
- const paramOptions = p.precompiledOptions || createParamOptions(p, 0);
2139
+ const paramOptions = p.precompiledOptions || createParamOptions(p, p.index ?? 0);
2267
2140
  return validateParam(app, ctx, rawValue, paramOptions, p.compiledValidator, p.compiledTypeConverter);
2268
2141
  });
2269
2142
  const syncResults = syncHandlers.map((h) => h(ctx));
@@ -2316,17 +2189,27 @@ var StrategyHandlerFactory = class {
2316
2189
  * Fallback: Generic async path
2317
2190
  */
2318
2191
  static createGenericAsyncHandler(params, app) {
2192
+ const allHaveExtractorOrFn = params.every((p) => !!p.precompiledExtractor || p.fn && typeof p.fn === "function");
2319
2193
  return async (ctx) => {
2320
- const sources = await extractParamSources(ctx, params);
2321
- const paramPromises = params.map((v, k) => {
2322
- let rawValue = extractValueFromSource(sources, v);
2323
- if (rawValue === null && v.fn) {
2324
- rawValue = v.fn(ctx, v.options);
2325
- }
2194
+ const sources = allHaveExtractorOrFn ? null : await extractParamSources(ctx, params);
2195
+ const paramPromises = params.map(async (v, k) => {
2196
+ if (v.precompiledExtractor) {
2197
+ const rawValue2 = await v.precompiledExtractor(ctx);
2198
+ const value = rawValue2 === void 0 && v.defaultValue !== void 0 ? v.defaultValue : rawValue2;
2199
+ const paramOptions2 = v.precompiledOptions || createParamOptions(v, v.index ?? k);
2200
+ return validateParam(app, ctx, value, paramOptions2, v.compiledValidator, v.compiledTypeConverter);
2201
+ }
2202
+ if (v.fn && typeof v.fn === "function") {
2203
+ const rawValue2 = await Promise.resolve(v.fn(ctx, v.options));
2204
+ const value = rawValue2 === void 0 && v.defaultValue !== void 0 ? v.defaultValue : rawValue2;
2205
+ const paramOptions2 = v.precompiledOptions || createParamOptions(v, v.index ?? k);
2206
+ return validateParam(app, ctx, value, paramOptions2, v.compiledValidator, v.compiledTypeConverter);
2207
+ }
2208
+ let rawValue = sources ? extractValueFromSource(sources, v) : void 0;
2326
2209
  if (rawValue === void 0 && v.defaultValue !== void 0) {
2327
- rawValue = v.defaultValue;
2210
+ return v.defaultValue;
2328
2211
  }
2329
- const paramOptions = v.precompiledOptions || createParamOptions(v, k);
2212
+ const paramOptions = v.precompiledOptions || createParamOptions(v, v.index ?? k);
2330
2213
  return validateParam(app, ctx, rawValue, paramOptions, v.compiledValidator, v.compiledTypeConverter);
2331
2214
  });
2332
2215
  return Promise.all(paramPromises);
@@ -2539,7 +2422,19 @@ var GraphQLRouter = class {
2539
2422
  rootValue: routeHandler,
2540
2423
  validationRules: validationRules.length > 0 ? validationRules : void 0,
2541
2424
  formatError: this.options.ext?.debug ? void 0 : (error) => {
2542
- return new Error(error.message);
2425
+ const formatted = {
2426
+ message: error.message
2427
+ };
2428
+ if (error.extensions) {
2429
+ formatted.extensions = error.extensions;
2430
+ }
2431
+ if (error.locations) {
2432
+ formatted.locations = error.locations;
2433
+ }
2434
+ if (error.path) {
2435
+ formatted.path = error.path;
2436
+ }
2437
+ return formatted;
2543
2438
  },
2544
2439
  context: /* @__PURE__ */ __name((req) => {
2545
2440
  return req.koattyContext;
@@ -3183,8 +3078,12 @@ var GrpcRouter = class {
3183
3078
  }
3184
3079
  DefaultLogger.Debug(`[GRPC_ROUTER] \u2705 Register request mapping: ["${path3}" => ${ctlItem.name}.${ctlItem.method}]`);
3185
3080
  impl[handler.name] = (call, callback) => {
3186
- DefaultLogger.Warn(`[GRPC_ROUTER] \u26A0\uFE0F Placeholder handler called for: ${handler.name} - this should not happen!`);
3187
- callback(new Error("This handler should not be called - routing should go through middleware"));
3081
+ const methodPath = `${si.name}/${handler.name}`;
3082
+ DefaultLogger.Warn(`[GRPC_ROUTER] Placeholder handler invoked for ${methodPath}. No controller matched this gRPC method.`);
3083
+ callback({
3084
+ code: 12,
3085
+ message: `Method ${methodPath} not implemented. Ensure a controller with @GrpcMethod('${handler.name}') is registered.`
3086
+ });
3188
3087
  };
3189
3088
  }
3190
3089
  if (Object.keys(impl).length > 0) {
@@ -3981,17 +3880,28 @@ function Get(name, defaultValue) {
3981
3880
  }
3982
3881
  __name(Get, "Get");
3983
3882
  function Post(name, defaultValue) {
3984
- return injectParam(async (ctx, opt) => {
3985
- const data = await bodyParser(ctx, opt);
3986
- const params = data.body ?? data;
3987
- return name ? params[name] : params;
3988
- }, "Post", ParamSourceType.BODY, name, defaultValue);
3883
+ return injectParam(
3884
+ // NOTE: This fn is a fallback path only. At runtime the strategy extractor uses
3885
+ // param.precompiledExtractor (generated by generatePrecompiledExtractor for BODY type)
3886
+ // which bypasses this fn entirely. This fn is invoked only when precompiledExtractor
3887
+ // is unavailable (e.g. startup compilation failure or CUSTOM source type fallback).
3888
+ async (ctx, opt) => {
3889
+ const data = await bodyParser(ctx, opt);
3890
+ if (name) return data[name];
3891
+ const { [FILE_KEY]: _files, ...bodyOnly } = data || {};
3892
+ return bodyOnly;
3893
+ },
3894
+ "Post",
3895
+ ParamSourceType.BODY,
3896
+ name,
3897
+ defaultValue
3898
+ );
3989
3899
  }
3990
3900
  __name(Post, "Post");
3991
3901
  function File(name, defaultValue) {
3992
3902
  return injectParam(async (ctx, opt) => {
3993
3903
  const body = await bodyParser(ctx, opt);
3994
- const params = body.file ?? {};
3904
+ const params = body[FILE_KEY] ?? {};
3995
3905
  return name ? params[name] : params;
3996
3906
  }, "File", ParamSourceType.FILE, name, defaultValue);
3997
3907
  }