Effect HttpApi 中间件错误 Schema 声明机制:从单错误到多错误数组的演进
2026/9/13 4:59:45 网站建设 项目流程

Effect HttpApi 中间件错误 Schema 声明机制:从单错误到多错误数组的演进

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

导读

本文基于 Effect 生态中effect包的 unstable HttpApi 模块,深入剖析一项类型系统与运行时行为并重的 API 变更:允许 HttpApi 中间件(HttpApiMiddleware)通过数组形式声明多个错误 Schema。变更后,中间件声明的错误在响应状态解析、客户端解码和生成的 API Schema 三个环节上,与端点(Endpoint)错误的行为完全对齐。读完本文,你将掌握 HttpApi 中间件错误声明的类型约束、运行时归一化实现,以及如何在认证、鉴权、限流等横切关注点中声明多错误契约。

变更背景:一个 changeset 背后的类型契约升级

本次变更记录于 .changeset/pre/calm-carrots-march.md:

--- "effect": patch --- Allow unstable HttpApi middleware to declare multiple error schemas with arrays. Middleware errors now follow endpoint error behavior for response status resolution, client decoding, and generated API schemas.

这是一次patch级别的变更:不破坏既有 API,而是放宽并统一中间件错误的表达能力。变更前,中间件只能声明单个错误 Schema;变更后,可以像端点那样声明一组 Schema(即错误联合),由框架负责:

  1. 响应状态解析(response status resolution):根据实际发生的错误类型,解析出对应的 HTTP 状态码;
  2. 客户端解码(client decoding):生成的客户端能够从响应体中按联合成员解码出正确的错误类型;
  3. 生成的 API Schema:OpenAPI 等派生 Schema 中正确呈现多个错误变体。

类型层:ErrorConstraintErrorSchemaFromConstraint

中间件错误声明在类型层面由HttpApiMiddleware.ts中的两个关键类型驱动(.repos/effect-smol/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts)。

错误约束:单个 Schema 或 Schema 数组

type ErrorConstraint = Schema.Top | ReadonlyArray<Schema.Top> type ErrorSchemaFromConstraint<E> = E extends ReadonlyArray<Schema.Constraint> ? E[number] : E extends Schema.Constraint ? E : never
  • ErrorConstraint表示中间件错误既可以是单个 Schema,也可以是只读 Schema 数组
  • ErrorSchemaFromConstraint是提取逻辑:当约束是数组时,取数组元素类型E[number]构成联合;当约束是单个 Schema 时,直接使用该 Schema。这一条件类型正是"多错误声明"的类型层核心——数组中的每个成员都会被并入中间件失败的错误通道。

贯穿三个模型:服务端、安全中间件、类型标识

ErrorConstraint被三个模型共用,保证多错误声明在整个声明体系中一致生效:

// 普通服务端中间件:失败通道为 unhandled | ErrorSchemaFromConstraint<E>["Type"] export type HttpApiMiddleware<Provides, E extends ErrorConstraint, Requires> = ( httpEffect: Effect.Effect<HttpServerResponse, unhandled, Provides>, options: { readonly endpoint: HttpApiEndpoint.Top; readonly group: HttpApiGroup.Top } ) => Effect.Effect<HttpServerResponse, unhandled | ErrorSchemaFromConstraint<E>["Type"], Requires | HttpRouter.Provided> // 安全中间件:每个 security scheme 的处理器拥有同样的错误约束 export type HttpApiMiddlewareSecurity<...> = { readonly [K in keyof Security]: (...) => Effect.Effect< HttpServerResponse, unhandled | ErrorSchemaFromConstraint<E>["Type"], Requires | HttpRouter.Provided > } // 类型标识:error 字段保存原始 ErrorConstraint(含数组形态) export interface AnyId { readonly [TypeId]: { readonly provides: any readonly requires: any readonly error: ErrorConstraint readonly clientError: any readonly requiredForClient: boolean } }

HttpApiMiddlewareSecurity的存在意味着:带 security 的中间件(如 Bearer 认证)同样支持多个错误 Schema——每个 scheme 的凭据解码与错误声明共享同一约束。这与 changeset 中"中间件错误遵循端点错误行为"的承诺一致。

提取工具类型:错误联合与编解码服务

ErrorSchema<A>出发,派生出一整套面向错误联合的提取工具:

export type ErrorSchema<A> = A extends { readonly [TypeId]: { readonly error: infer E } } ? ErrorSchemaFromConstraint<E> : never export type Error<A> = ErrorSchema<A>["Type"] export type ErrorServicesEncode<A> = ErrorSchema<A>["EncodingServices"] export type ErrorServicesDecode<A> = ErrorSchema<A>["DecodingServices"]
  • Error<A>:中间件失败时可能抛出的解码后错误类型联合
  • ErrorServicesEncode/ErrorServicesDecode:错误 Schema 各自携带的编码/解码服务需求,会被自动并入中间件实现与生成的客户端所需环境中。

运行时层:Service构造器与getError归一化

类型约束之外,运行时实现同样处理了"数组 vs 单个"两种形态。Service构造器(用于创建中间件服务类)的签名与实现如下:

export const Service = <Self, Config extends { requires?: any; provides?: any; clientError?: any } = ...>(): < const Id extends string, const Error extends ErrorConstraint = never, // 错误约束默认 never const Security extends Record<string, HttpApiSecurity.HttpApiSecurity> = never, RequiredForClient extends boolean = false >(id: Id, options?: { readonly error?: Error | undefined readonly security?: Security | undefined readonly requiredForClient?: RequiredForClient | undefined } | undefined) => ServiceClass<...> => (id: string, options?) => { // ... self.error = getError(options?.error) self.requiredForClient = options?.requiredForClient ?? false if (options?.security !== undefined) { if (Object.keys(options.security).length === 0) { throw new Error("HttpApiMiddleware.Service: security object must not be empty") } // ... } return self }

getError:把任意输入归一化为只读错误集合

function getError(error: ErrorConstraint | undefined): ReadonlySet<Schema.Top> { if (error === undefined) return new Set() return new Set(Array.isArray(error) ? error : [error]) }

这是本次变更在运行时的落点:

  • error未声明 → 空集合(不产生错误契约);
  • 声明单个 Schema → 包装为单元素集合;
  • 声明 Schema 数组 → 展开为多元素集合。

ServiceClass的静态侧通过readonly error: ReadonlySet<Schema.Top>暴露该集合,供端点到中间件的校验、客户端生成与 OpenAPI 派生逻辑消费。AnyService接口同样以ReadonlySet<Schema.Top>保存错误集合,确保所有消费方看到统一的运行时视图。

错误如何被实际应用:HttpApiBuilder的中间件流水线

中间件声明只是契约,真正被"执行"是在HttpApiBuilder构建路由时。在 .repos/effect-smol/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts 中,applyMiddleware遍历端点挂载的中间件并按序套用:

const applyMiddleware = <Group extends HttpApiGroup.Constraint, A extends Effect.Effect<any, any, any>>( // ... ) => { // ... for (const key_ of endpoint.middlewares) { const key = key_ as HttpApiMiddleware.AnyService const apply = HttpApiMiddleware.isSecurity(key) ? makeSecurityMiddleware(key, service) // 安全中间件走专用路径 : // 普通中间件套用 httpEffect => ... 的包装逻辑 } }
  • 普通中间件:把端点响应 Effect 包装为httpEffect => ...形式,失败的错误通道中并入中间件声明的ErrorSchemaFromConstraint<E>["Type"]
  • 安全中间件:通过makeSecurityMiddleware为每个 security scheme 的凭据解码分配独立的处理器,securityMiddlewareCache(WeakMap)避免重复构建。

因此,当中间件以数组声明多个错误 Schema 时,端点执行链路中的失败要么来自端点自身、要么来自中间件联合中的任意成员——二者在HttpApiBuilder的错误通道中被统一处理,这正是 changeset 所述"遵循端点错误行为"的架构含义。

实战示例:用数组声明多个错误 Schema

结合 layerSchemaErrorTransform(把HttpApiSchemaError转换为自定义错误的层),演示多错误声明:

import { Effect, Schema } from "effect" import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiMiddleware } from "effect/unstable/httpapi" // 1. 定义两个不同的错误变体 class RateLimited extends Schema.TaggedError<RateLimited>()("RateLimited", { retryAfterSeconds: Schema.Number }) {} class Unauthorized extends Schema.TaggedError<Unauthorized>()("Unauthorized", {}) {} // 2. 以“数组”形式声明中间件可产生的多个错误 Schema class AuthGuard extends HttpApiMiddleware.Service<AuthGuard>()("api/AuthGuard", { error: [Unauthorized, RateLimited] // 数组 → 错误联合 }) {} // 3. 将端点产生的 schema 错误转换为中间件声明的错误 const AuthGuardLayer = HttpApiMiddleware.layerSchemaErrorTransform( AuthGuard, (schemaError) => Effect.fail(schemaError.kind === "RateLimited" ? new RateLimited({ retryAfterSeconds: 30 }) : new Unauthorized()) ) // 4. 端点与组正常声明 const endpoint = HttpApiEndpoint.get("example", "/").addError(RateLimited) const group = HttpApiGroup.make("examples").add(endpoint)

要点说明:

  • error选项的类型为ErrorConstraint,即Schema.Top | ReadonlyArray<Schema.Top>,数组形式与单个形式均合法;
  • 声明后,AuthGuard的静态error集合包含两个成员,Error<AuthGuard>的类型为Unauthorized | RateLimited
  • 生成的客户端将按端点错误同样的机制,从响应中解码出对应错误变体,生成的 API Schema 也会同时呈现两个错误响应。

测试与类型验证覆盖

多错误声明的能力已进入仓库的测试与类型测试矩阵:

  • .repos/effect-smol/packages/effect/typetest/unstable/httpapi/HttpApiMiddleware.tst.ts:针对中间件错误约束、客户端错误、ForClient标记做编译期断言;
  • .repos/effect-smol/packages/effect/typetest/unstable/httpapi/HttpApiBuilder.tst.ts:验证中间件声明在组/端点构建中的类型收敛;
  • .repos/effect-smol/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts 与 .repos/effect-smol/packages/platform/node/test/HttpApi.test.ts:覆盖中间件套用、安全凭据处理与运行时错误路径;
  • .repos/effect-smol/packages/effect/test/unstable/httpapi/OpenApi.test.ts 与 .repos/effect-smol/packages/platform/node/test/OpenApi.test.ts:验证错误联合正确投影到生成的 OpenAPI Schema。

若需在既有代码中启用该能力,注意HttpApi 模块目前位于unstable命名空间(导入路径为effect/unstable/httpapi),HttpApiMiddleware.Service要求@since 4.0.0,即effect4.x 及以上版本,并确认该 changeset 已随目标版本发布。

总结

本次 changeset 揭示的是一次"小改动、大统一"的类型系统演进:

  1. 声明能力HttpApiMiddleware.Serviceerror选项从单个 Schema 扩展为Schema.Top | ReadonlyArray<Schema.Top>,数组中的每个成员都成为中间件错误联合的组成部分;
  2. 类型推导ErrorSchemaFromConstraint在类型层将数组约束展开为联合,Error<A>ErrorServicesEncode/Decode<A>随之生效;
  3. 运行时归一化getError将数组/单个/未声明统一收敛为ReadonlySet<Schema.Top>,供服务类静态元数据使用;
  4. 行为对齐:中间件错误在响应状态解析、客户端解码、生成 API Schema 三个环节与端点错误完全一致,并由HttpApiBuilder.applyMiddleware统一并入错误通道。

对于使用 Effect 构建 schema 驱动 HTTP API 的团队,这意味着认证、限流、授权等横切中间件现在可以像端点一样表达完整的错误契约,无需再依赖单个错误的妥协方案。

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询