十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

t3code:Effect Unstable HttpApi 中 HttpApiBuilder 的运行时错误诊断深入解析

t3code:Effect Unstable HttpApi 中 HttpApiBuilder 的运行时错误诊断深入解析 t3codeEffect Unstable HttpApi 中 HttpApiBuilder 的运行时错误诊断深入解析【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code本文基于 t3code 仓库内 effect-smol 的一个 changeset.repos/effect-smol/.changeset/pre/eff-700-httpapi-middleware-errors.md解析 Effectunstable/HttpApi栈中HttpApiBuilder的两项运行时故障诊断改进缺失的服务中间件现在会报出明确的Service not found: middleware错误缺失的 group 实现则会给出具体的修复建议与可用 group 清单。读完本文你将理解HttpApiBuilder.layer/group/applyMiddleware的底层调用链、错误信息的真实来源并能据此正确组装 Layer、在出错时快速定位根因。一、背景HttpApiBuilder 在 unstable HttpApi 栈中的位置HttpApiBuilder位于 HttpApiBuilder.ts其模块头注释定义了它的职责Builds server routes from declarativeHttpApicontracts. At runtime it decodes request parts with schemas, runs middleware and security handlers, invokes the registered endpoint handler, and encodes successes or declared errors intoHttpServerResponsevalues.since 4.0.0即把声明式的HttpApi契约 各 group 的 handler 实现转换成HttpRouter路由。运行时链路为HttpApiBuilder.group(api, identifier, build)为单个 group 构建 handler 集合把每个端点经handlerToRoute转成路由并产出一个以 group key 为键的Context见 HttpApiBuilder.ts#L126-L161HttpApiBuilder.layer(api, options?)从运行时上下文中按 key 收集所有 group 的路由合并挂到HttpRouterHttpApiBuilder.ts#L63-L112每个端点的内部 effect 由handlerToHttpEffect构建解码 params/headers/query/payload →applyMiddleware包裹中间件 → 调用 handler → 编码 success/declared errorHttpApiBuilder.ts#L756-L839。本次 changeset 针对的正是第 2 步group 缺失与第 3 步中中间件解析middleware 服务缺失这两类运行时故障它们不会在编译期暴露Layer 组合漏掉某个依赖属于运行期组装错误此前只能看到晦涩的报错。二、缺失 middleware 服务从 is not a function 到 Service not found2.1 问题本质HttpApiMiddleware是一个Context.Tag服务标识。端点上声明了.middleware(M)后applyMiddleware会在处理每个请求时从当前Context中取出服务M并调用它。若 Layer 组合时忘了Layer.provide(M 的实现)旧实现拿到的是undefined再当作函数调用最终抛出一个与根因无关的xxx is not a functionTypeError——对定位问题几乎没有帮助。2.2 源码实现Context.getUnsafe 强制解析改进后的applyMiddlewareHttpApiBuilder.ts#L856-L872const applyMiddleware Group extends HttpApiGroup.Constraint, A extends Effect.Effectany, any, any( group: Group, endpoint: HttpApiEndpoint.Top, context: Context.Contextany, handler: A ) { const options { group, endpoint } for (const key_ of endpoint.middlewares) { const key key_ as HttpApiMiddleware.AnyService // 关键改动通过 Context.getUnsafe 从上下文取服务 // 缺失时立即抛出 Service not found: middleware defect const service Context.getUnsafe(context, key) const apply HttpApiMiddleware.isSecurity(key) ? makeSecurityMiddleware(key, service) : service handler apply(handler, options) } return handler }要点Context.getUnsafe在 key 不存在时直接抛错而不是返回undefined其错误文案生成于 Context.ts#L1049Service not found${service.key ? : ${String(service.key)} : }因此报错形如Service not found: Server/MissingMiddleware服务名直接来自HttpApiMiddleware.ServiceM()(Server/MissingMiddleware)的命名参数安全中间件HttpApiMiddleware.Security走makeSecurityMiddleware分支会额外把securityDecode解码出的凭据传给各 security 分支HttpApiBuilder.ts#L879-L924普通中间件则直接以(effect, options) effect的形式包裹 handleroptions携带{ group, endpoint }元数据循环按endpoint.middlewares数组顺序依次包裹先注册的 middleware 被包在内层后注册的在外层、先执行。回归测试断言的调用顺序m2:health.health, m1:health.health, ...M2 后于 M1 注册却先执行印证了这一点HttpApi.test.ts#L625-L630。2.3 回归测试验证测试 missing middleware layer fails with service not found errorHttpApi.test.ts#L634-L669构造了一个声明了middleware(M)但 Layer 组合中不提供M的 APIclass M extends HttpApiMiddleware.ServiceM()(Server/MissingMiddleware) {} const Api HttpApi.make(api).add( HttpApiGroup.make(group) .add(HttpApiEndpoint.get(a, /a, { success: Schema.String })) .middleware(M) ) const GroupLayer HttpApiBuilder.group( Api, group, (handlers) handlers.handle(a, () Effect.succeed(ok)) ) const ApiLayer HttpRouter.serve( HttpApiBuilder.layer(Api).pipe(Layer.provide(GroupLayer)), { disableListenLog: true, disableLogger: true } ).pipe(Layer.provideMerge(NodeHttpServer.layerTest)) // 请求 /a沙箱化捕获 defect return HttpClient.get(/a).pipe( Effect.provide(ApiLayer), Effect.sandbox, Effect.flip, Effect.flatMap((cause) Effect.sync(() { const defect Cause.squash(cause) assert.instanceOf(defect, Error) assert.include(defect.message, Service not found: Server/MissingMiddleware) assert.isFalse(defect.message.includes(is not a function)) }) ) )注意测试显式断言错误信息不包含is not a function——这正是对旧行为的直接回归保护。三、缺失 group 实现可操作的错误上下文3.1 layer 的查找逻辑HttpApiBuilder.layer在运行时遍历api.groups按group.key从上下文 Map 中取路由取不到时不再静默继续而是携带四类信息终止HttpApiBuilder.ts#L86-L100const services yield* Effect.context...() // 从上下文 key 中筛出所有已注册的 group 服务作为“可用清单” const availableGroups Array.from(services.mapUnsafe.keys()).filter((key) key.startsWith(effect/httpapi/HttpApiGroup/) ) const groups Object.values(api.groups) as ReadonlyArrayHttpApiGroup.Top for (const group of groups) { const groupRoutes services.mapUnsafe.get(group.key)?.routes as ArrayHttpRouter.Routeany, any if (groupRoutes undefined) { const available availableGroups.length 0 ? none : availableGroups.join(, ) return yield* Effect.die( HttpApiGroup ${group.identifier} not found (key: ${group.key}). Did you forget to provide HttpApiBuilder.group(api, ${group.identifier}, ...)? Available groups: ${available} ) } routes.push(...groupRoutes) } yield* (router.addAll(routes) as Effect.Effectvoid)报错信息包含四要素全部可机械地用于修复要素示例值作用group 标识符HttpApiGroup health not found定位是哪个 group 没实现服务 key(key: effect/httpapi/HttpApiGroup/health)对照Context实际提供的 key建议调用Did you forget to provide HttpApiBuilder.group(api, health, ...)?直接给出应补的调用形态可用 group 清单Available groups: effect/httpapi/HttpApiGroup/users区分完全没给与给错了对象group key 的前缀约定effect/httpapi/HttpApiGroup/identifier由group产出的Context.makeUnsafe(new Map([[group.key, { routes, handlers }]]))保证HttpApiBuilder.ts#L155-L160layer正是依赖该前缀做可用 group枚举。3.2 为什么 addHttpApi 场景尤其需要这个诊断HttpApi.make(...).addHttpApi(otherApi)会把另一个 API 的所有 group 合并进当前 API但路由实现层不会随之自动合并——每个 group 仍须各自通过HttpApiBuilder.group提供。这意味着被合并 API 的 group 很容易漏掉实现层。回归测试 missing addHttpApi group layer has actionable errorHttpApi.test.ts#L672-L713覆盖的正是这一形态Api由HealthApigrouphealthaddHttpApi(V0)groupusers合成Layer 只提供了UsersLayer断言错误串同时包含三段assert.include(defect, HttpApiGroup \health\ not found) assert.include(defect, HttpApiBuilder.group(api, \health\, ...)) assert.include(defect, Available groups: effect/httpapi/HttpApiGroup/users)3.3 正向对照addHttpApi API 级 middleware 跨合并 group 生效同一批回归测试中addHttpApi middleware works across merged groupsHttpApi.test.ts#L562-L632验证了正常路径const V0 HttpApi.make(v0).add( HttpApiGroup.make(users).add( HttpApiEndpoint.get(list, /users, { success: Schema.String }) ) ) const Api HttpApi.make(api) .add( HttpApiGroup.make(health).add( HttpApiEndpoint.get(health, /health, { success: Schema.String }) ) ) .addHttpApi(V0) .middleware(M1) // API 级 middleware声明在合并后的 Api 上 .middleware(M2) const HealthLayer HttpApiBuilder.group(Api, health, (handlers) handlers.handle(health, () Effect.succeed(ok))) const UsersLayer HttpApiBuilder.group(Api, users, (handlers) handlers.handle(list, () Effect.succeed(ok))) const M1Layer Layer.succeed(M1, (effect, { endpoint, group }) Effect.sync(() calls.push(m1:${group.identifier}.${endpoint.identifier})).pipe( Effect.flatMap(() effect) )) // M2Layer 同理 ... const ApiLayer HttpRouter.serve( HttpApiBuilder.layer(Api).pipe( Layer.provide(HealthLayer), Layer.provide(UsersLayer), Layer.provide(M1Layer), Layer.provide(M2Layer) ), { disableListenLog: true, disableLogger: true } ).pipe(Layer.provideMerge(NodeHttpServer.layerTest))测试随后断言/health与/users均返回 200且中间件调用序列为assert.deepStrictEqual(calls, [ m2:health.health, m1:health.health, m2:users.list, m1:users.list ])这说明声明在合并后Api上的 API 级 middleware对addHttpApi引入的 groupusers与本 API 自有 grouphealth统一生效且每次调用都携带正确的{ group, endpoint }上下文。这也是新增诊断的价值所在——跨合并 group 的组装一旦漏掉 middleware 层或某个 group 层报错会精确指向缺失的那一项而不是笼统失败。四、实操建议HttpApi 服务的 Layer 组装清单综合上述源码与测试一个可运行的 HttpApi 服务器至少需要四类 Layer缺一类就会触发对应的运行时故障每个 group 一个实现层HttpApiBuilder.group(api, groupIdentifier, (handlers) handlers.handle(...))并通过Layer.provide挂到HttpApiBuilder.layer(api)上。漏掉 →HttpApiGroup x not found (key: ...)defect错误中直接给出建议调用与Available groups清单。每个 middleware 一个服务层对HttpApiMiddleware.Service提供Layer.succeed(Tag, (effect, { endpoint, group, credential? }) ...)。漏掉 →Service not found: Tag 名称defect不再出现is not a function。平台依赖HttpApiBuilder.layer的环境还包含HttpRouter、HttpPlatform、FileSystem、Path、Etag.Generator见 HttpApiBuilder.ts#L63-L77通常由NodeHttpServer.layerTest/HttpServer.layerServices等平台 Layer 满足。可选 OpenAPI 路由HttpApiBuilder.layer(api, { openapiPath: /openapi.json })会额外注册一个GET /openapi.json路由返回OpenApi.fromApi(api)的 JSON 文档HttpApiBuilder.ts#L102-L111便于调试时对照契约与实际行为。另外两个与错误边界相关的实现细节值得知晓Handlers集合在注册阶段就做了编译期外的运行时兜底端点标识符不存在于 group 时抛HttpApiEndpoint id not found in HttpApiGroup group重复注册时抛Handler for HttpApiEndpoint id is already registeredHttpApiBuilder.ts#L590-L613——这两类错误与本次改动互补共同覆盖了声明与实现不一致的全部组合错误。handler 的失败处理上handlerToHttpEffect末尾把HttpApiSchemaError解码类错误走Effect.die其余错误按端点声明的 errors schema 编码进响应体HttpApiBuilder.ts#L831-L838而本次两处新诊断middleware/group 缺失都是 Layer 组装错误因此以 defect 形式在请求处理时暴露属于fail fast 到日志/遥测而非返回给客户端的策略。五、适用前提与限制该 changeset 元数据声明effect: patcheff-700-httpapi-middleware-errors.md即对effect包的补丁级修订不改变公开 API 签名只改变故障时的错误形态依赖旧错误串做匹配的代码如断言is not a function需要同步调整。HttpApiBuilder位于unstable目录since 4.0.0从源码结构看该模块接口仍可能随 Effect 4.x 演进引用其内部行为如effect/httpapi/HttpApiGroup/key 前缀时应视为实现细节而非稳定契约。文中所有行为均以仓库内源码与回归测试为准实现见 HttpApiBuilder.ts错误文案来源见 Context.ts#L1049完整测试用例见 HttpApi.test.ts。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表