观察类发现,来自 #5822(PR #6303)的实现过程,按 Prime Directive #10 单独立单。未认领。
今天没有用户会撞到:需要客户端重复传同一个查询参数才会触发。
事实
IHttpRequest.query 的契约是 Record<string, string | string[]>(packages/spec/src/contracts/http-server.ts),
即重复的查询参数会是数组。package-routes.ts 的两个 handler 直接把它当字符串用:
GET /api/v1/packages/:id — const version = req.query?.version || 'latest'; … packageService.get(packageId, version)
DELETE /api/v1/packages/:id — const version = req.query?.version; … packageService.delete(packageId, version)
PackageService.get/delete 的形参是 version?: string。所以 ?version=a&version=b 会把
['a','b'] 交给服务层,查询按一个不存在的版本走(或按适配器的字符串化行为走),
而 DELETE 还会因为 version 为真而跳过 protocol.deletePackage 的完整卸载分支
(if (!version && typeof options.protocol?.deletePackage === 'function'),退化成窄语义的版本删除。
为什么没有被门禁抓到
tsc --noEmit -p packages/rest/tsconfig.json 确实报这两条:
src/package-routes.ts(178,55): error TS2345: Argument of type 'string | string[]' is not assignable to parameter of type 'string | undefined'.
src/package-routes.ts(245,61): error TS2345: Argument of type 'string | string[]' is not assignable to parameter of type 'string | undefined'.
(行号为 origin/main@f6609e6ae;PR #6303 之后同两个调用点在 207/279。)
但 packages/rest 没有 typecheck 脚本 —— 它是 scripts/check-type-check-coverage.mjs
的 DEBT 条目(#4311,457 条冻结原始错误之二),所以 CI 里没有任何 job 编译它,vitest 与 tsup
也都不做类型检查。这两条属于该冻结计数,不是新引入的;本单记的是它们背后具体的运行时行为。
处置建议(留给分诊)
窄修:在两个 handler 里按契约取第一个值(或明确拒收数组,400 MISSING_REQUIRED_FIELD 家族),
让类型与运行时一致 —— 注意这属于 consumer 侧的入参规范化,不是给 off-spec 输入加宽容别名:
契约本来就说这里可能是数组,是消费方没有处理声明过的形状。
顺带值得问的:同一形状在 rest 其他读 req.query.* 的地方是否也存在(未逐一排查,本单不扩大范围)。
查重
open issue 搜过 packages/rest + query / version / TS2345,无同题单。
#4311 是类型检查覆盖率的总账(计数已含这两条),本单是其中一条的行为面,非重复。
观察类发现,来自 #5822(PR #6303)的实现过程,按 Prime Directive #10 单独立单。未认领。
今天没有用户会撞到:需要客户端重复传同一个查询参数才会触发。
事实
IHttpRequest.query的契约是Record<string, string | string[]>(packages/spec/src/contracts/http-server.ts),即重复的查询参数会是数组。
package-routes.ts的两个 handler 直接把它当字符串用:GET /api/v1/packages/:id—const version = req.query?.version || 'latest'; … packageService.get(packageId, version)DELETE /api/v1/packages/:id—const version = req.query?.version; … packageService.delete(packageId, version)PackageService.get/delete的形参是version?: string。所以?version=a&version=b会把['a','b']交给服务层,查询按一个不存在的版本走(或按适配器的字符串化行为走),而
DELETE还会因为version为真而跳过protocol.deletePackage的完整卸载分支(
if (!version && typeof options.protocol?.deletePackage === 'function'),退化成窄语义的版本删除。为什么没有被门禁抓到
tsc --noEmit -p packages/rest/tsconfig.json确实报这两条:(行号为
origin/main@f6609e6ae;PR #6303 之后同两个调用点在 207/279。)但
packages/rest没有typecheck脚本 —— 它是scripts/check-type-check-coverage.mjs的 DEBT 条目(#4311,457 条冻结原始错误之二),所以 CI 里没有任何 job 编译它,vitest 与 tsup
也都不做类型检查。这两条属于该冻结计数,不是新引入的;本单记的是它们背后具体的运行时行为。
处置建议(留给分诊)
窄修:在两个 handler 里按契约取第一个值(或明确拒收数组,400
MISSING_REQUIRED_FIELD家族),让类型与运行时一致 —— 注意这属于 consumer 侧的入参规范化,不是给 off-spec 输入加宽容别名:
契约本来就说这里可能是数组,是消费方没有处理声明过的形状。
顺带值得问的:同一形状在 rest 其他读
req.query.*的地方是否也存在(未逐一排查,本单不扩大范围)。查重
open issue 搜过
packages/rest+query/version/TS2345,无同题单。#4311 是类型检查覆盖率的总账(计数已含这两条),本单是其中一条的行为面,非重复。