ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Shields 徽章服务输入数据校验(Input Validation)实战指南:从 Joi Schema 设计到 InvalidResponse 异常处理

Shields 徽章服务输入数据校验(Input Validation)实战指南:从 Joi Schema 设计到 InvalidResponse 异常处理 Shields 徽章服务输入数据校验Input Validation实战指南从 Joi Schema 设计到 InvalidResponse 异常处理【免费下载链接】shieldsConcise, consistent, and legible badges in SVG and raster format项目地址: https://gitcode.com/gh_mirrors/sh/shields导读本文以 Shields 徽章服务badges/shields的 doc/input-validation.md 为核心指南系统讲解该开源项目如何对上游 API 返回的原始数据进行校验防止渲染出null、NaN、undefined等脏徽章。你将掌握基于 Joi 定义输入 Schema 的完整方法论、与 InvalidResponse 异常体系配合的手工校验技巧、以及描述性而非规定性的 Schema 设计原则并通过仓库源码与真实服务如 crates.io的实现案例获得可直接复用的实战方案。为什么 Shields 需要对上游数据做输入校验Shields 的每个徽章服务Service都要向后端各类上游 API 发起请求拿到版本号、覆盖率、构建状态、许可证等原始数据后再渲染成 SVG 徽章。上游 API 返回的数据是不可信的输入——它可能缺失字段、类型不符、格式怪异甚至直接返回错误对象。如果不做校验渲染阶段就会抛出运行时异常或者把异常值直接画到徽章上例如![](https://img.shields.io/badge/version-null-blue)版本号显示为null![](https://img.shields.io/badge/coverage-NaN%25-red)覆盖率显示为NaN%![](https://img.shields.io/badge/build-undefined-red)构建状态显示为undefined![](https://img.shields.io/badge/coverage---10%25-critical)覆盖率出现负数从 doc/input-validation.md 可以看到输入校验承担三个核心职责确保渲染徽章时不会抛出运行时错误例如对undefined调用.split()确保徽章不会渲染出虚假或意外的输出杜绝null、NaN、undefined等污染用户 README表达并记录我们对输入数据的理解——Schema 本身就是一份可执行的接口契约文档。默认校验机制Joi SchemaShields 的默认校验机制是使用 Joi 为输入数据定义 Schema。校验逻辑被实现于基础类base classes中凡是继承这些基础类的服务类都会自动获得校验能力无需在每个服务里重复编写校验代码。从源码结构看整个校验链路是这样串联起来的core/base-service/validate.js 提供了通用的validate()函数是校验的核心实现core/base-service/base.js 中的静态方法_validate()包装了validate()并将错误类固定为InvalidResponse用于校验上游响应数据core/base-service/base-json.js 的_requestJson()在拿到并解析 JSON 之后会调用this.constructor._validate(json, schema)把请求—解析—校验串成一条流水线。看一下validate()的底层实现core/base-service/validate.jsfunction validate( { ErrorClass, prettyErrorMessage data does not match schema, includeKeys false, traceErrorMessage Data did not match schema, traceSuccessMessage Data after validation, }, data, schema, ) { if (!schema || !Joi.isSchema(schema)) { throw Error(A Joi schema is required) } const options { abortEarly: false, allowUnknown: true, stripUnknown: true } const { error, value } schema.validate(data, options) if (error) { trace.logTrace(validate, emojic.womanShrugging, traceErrorMessage, error.message) let prettyMessage prettyErrorMessage if (includeKeys) { const keys error.details.map(({ path }) path) if (keys) { prettyMessage ${prettyErrorMessage}: ${keys.join(, )} } } throw new ErrorClass({ prettyMessage, underlyingError: error }) } else { trace.logTrace(validate, emojic.bathtub, traceSuccessMessage, value, { deep: true }) return value } }值得注意的三个校验选项schema.validate的 optionsabortEarly: false一次报告所有校验错误而不是遇到第一个错误就停止方便开发者一次性看清数据全部问题allowUnknown: true允许数据中存在 Schema 未声明的字段符合只校验我们依赖的字段的原则stripUnknown: true校验通过后未声明的字段会被从返回值中剥离确保下游渲染拿到的数据是净化后的最小集合。该校验行为有完整的单元测试覆盖见 core/base-service/validate.spec.js测试验证了缺少 Schema 时抛出A Joi schema is required、数据不匹配时抛出InvalidParameter并记录 trace、以及允许但剥离未知字段allows but strips unknown keys等行为。什么时候需要手工校验抛出 InvalidResponseJoi 并非万能。当需要强制某个 Joi 及其插件无法表达的约束时例如跨字段的关联条件、必须依赖业务逻辑才能判断的关系就需要手工实现校验。手工校验失败时应抛出InvalidResponse异常而不是任其产生运行时错误。InvalidResponse定义在 core/base-service/errors.js它继承自抽象的ShieldsRuntimeError默认的对外消息是invalidclass InvalidResponse extends ShieldsRuntimeError { get name() { return InvalidResponse } get defaultPrettyMessage() { return invalid } constructor(props {}) { const message props.underlyingError ? Invalid Response: ${props.underlyingError.message} : Invalid Response super(props, message) this.response props.response } }手工校验抛InvalidResponse的典型案例位于 services/crates/crates-base.js 的getVersionObj()当响应里声明了最新版本却在versions数组中找不到对应版本对象时直接throw new InvalidResponse({ prettyMessage: version not found })——这是一个纯 Joi 难以表达的数据一致性约束所以交给代码逻辑处理。InvalidResponse异常对象支持prettyMessage显示在徽章上的用户友好文本、underlyingError包装的原始错误、response上游响应上下文和cacheSeconds错误响应缓存时长等属性。抛出后由基础类的错误处理机制捕获渲染成带错误提示的徽章而不是让服务崩溃见 core/base-service/base.js 的_handleError。Schema 设计原则由我们要如何使用数据决定doc/input-validation.md 明确强调Schema/校验的选择由我们对数据所做的假设决定即用到什么就校验什么。核心原则包括要使用某个值就确保它存在→Joi.string().required()要对它做乘法运算就检查它是数字→Joi.number()要对它调用.split()就确保它是字符串→Joi.string()要访问foo[0]foo必须是数组→Joi.array()要按 semver 假设对版本排序就先校验它是 semver→ 使用Joi.string().regex(/^v?\d\.\d\.\d/)之类的约束或专门的正则 Schema反之不依赖的特性就不需要校验。例如如果只是把 API 返回的版本号原样渲染到徽章上既不排序也不转换那么版本号具体是什么格式并不重要用一个非常宽松的 Schema 就够了比如Joi.string().required()。这种最小必要校验理念在真实服务中得到充分体现。例如 crates.io 服务的 Schemaservices/crates/crates-base.jsconst versionSchema Joi.object({ downloads: nonNegativeInteger, crate_size: nonNegativeInteger, num: Joi.string().required(), license: Joi.string().required().allow(null), rust_version: Joi.string().allow(null), }) const crateResponseSchema Joi.object({ crate: Joi.object({ downloads: nonNegativeInteger, recent_downloads: nonNegativeInteger.allow(null), max_version: Joi.string().required(), }).required(), versions: Joi.array().items(versionSchema).min(1).required(), }).required()可以看到num版本号只要Joi.string().required()即可因为徽章只需原样渲染它而license这类可能为null的字段用.allow(null)显式放行——这就是不严于上游定义的体现。其中的nonNegativeInteger是项目在 services/validators.js 中封装的通用校验器可以被多个服务复用。描述性而非规定性现实世界优先于文档Shields 的校验哲学是描述性descriptive而非规定性prescriptive它如实反映所服务社区的现实规范而不是强加理想化的接口约定。这一原则直接决定了以下实践共享 Schema 是允许的可以定义一个 Schema 同时应用于多个徽章。例如const schema Joi.object({ license: Joi.string().required(), version: Joi.string().required(), }).required()上面的 license 徽章和 version 徽章可以共用这个 Schema 校验各自的上游响应。文档与真实响应冲突时以真实世界为准如果上游文档声称 version 是 semver但现实中存在版本号为0.3b或1.2.1.27的包那么应优先接受这些真实值而不是强制按文档行为校验。校验失败不应阻止渲染Schema 校验失败只应发生在该字段对渲染徽章是必需的时。仍以上述共享 Schema 为例如果我们发现现实中有包存在version键但没有license键那么应该拆分 Schema或把version设为可选并在代码中处理缺失而不是因为一个非必需字段的缺失而拒绝渲染整个徽章。构建状态徽章复用共享的 isBuildStatus 校验器对于构建状态类徽章Shields 提供了共享的isBuildStatus校验器实现于 services/build-status.js绝大多数构建状态徽章都应使用它做输入校验并用配套的renderBuildStatusBadge做渲染。任何额外的状态值都可以添加到对应的颜色数组中。isBuildStatus的定义本质上是const isBuildStatus Joi.equal(...allStatuses)其中allStatuses由四类状态合并而成services/build-status.js类别状态值示例渲染颜色greenStatuses绿fixed、passed、passing、succeeded、success、successfulbrightgreen消息统一为passingorangeStatuses橙partially succeeded、unstable、timeoutorangeredStatuses红broken、error、errored、failed、failing、failure、infrastructure_failureredfailed会归一化为failingotherStatuses灰/默认building、canceled、pending、queued、running、scheduled、skipped、waiting等保留原始状态文本renderBuildStatusBadge负责把校验通过的状态映射为徽章上的message colorservices/build-status.js。这套共享机制的收益在于各 CI 服务如 Travis、CircleCI、AppVeyor 等即使上游状态枚举各异也能统一归一化到 Shields 的标准状态词汇从而保证徽章在视觉和语义上的一致性。当某服务遇到上游新增的状态值时只需把它加进对应的颜色数组就能继续复用整套渲染逻辑。辅助工具与工作流建议Schema 逆向工程工具https://joi.dev/tester/ 可以根据一个真实 API 响应自动反推出 Joi Schema是很好的起点。以此为起点时记得删掉那些渲染徽章并不依赖的字段——这正是allowUnknown选项存在的意义。校验宽松度定位如果某个徽章显示version-null、coverage-NaN%、build-undefined等异常值或者因为未处理的上游数据而抛出未捕获的运行时异常说明对应的输入校验已失效broken需要修复。保持不严于上游的尺度license可以为null就写.allow(null)API 可能返回0.3b这样的版本就不要强制 semver——校验的目的是如实反映数据、安全渲染徽章而不是替上游 API 制定规范。小结一条可复制的校验方法论综合 doc/input-validation.md 与仓库实现Shields 的输入校验方法论可以归纳为一条清晰的工作流默认用 Joi 定义 Schema由BaseService._validate()及_requestJson()等基础类方法自动执行参见 core/base-service/base-json.jsSchema 严格程度以我们将如何使用数据为准用到的字段校验存在性与类型用不到的字段不校验Joi 无法表达的约束才手工校验失败时统一抛InvalidResponsecore/base-service/errors.js现实世界 API 响应优先于接口文档保持描述性而非规定性可复用就复用跨徽章共享 Schema、复用isBuildStatus与renderBuildStatusBadge等公共组件校验失败不影响渲染非必需字段缺失时拆分或放宽 Schema而不是拒渲染。按照这套方法论新增一个徽章服务时只需声明式地写下一个 Joi Schema即可自动获得健壮的输入防御——既保证徽章永不渲染null/NaN/undefined等污染数据也让每一次对上游接口的理解都有据可查、可测试、可演进。【免费下载链接】shieldsConcise, consistent, and legible badges in SVG and raster format项目地址: https://gitcode.com/gh_mirrors/sh/shields创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表