t3code 依赖的 Effect 4.0:内置 HttpApiError 错误类实现 HttpServerRespondable,可在普通 HTTP 服务端直接返回
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
本文基于 t3code 仓库内置参考仓库.repos/effect-smol中的 changeset 变更记录eff-701-httpapierror-respondable展开:Effect 4.0 让全部内置HttpApiError错误类实现HttpServerRespondable协议,使这些错误值不再只能服务于HttpApi声明式框架,而是可以直接从普通 HTTP 服务端处理函数中返回,由运行时自动渲染为对应的状态码响应。读完本文,你能掌握该变更的具体内容、内置错误类与状态码的完整映射、Respondable协议的转换机制,以及它在普通 handler 错误中间件中的实际调用链。
变更记录本身:一段 changeset 描述了什么
关联文档是一份标准的 changesets 变更记录,位于 .repos/effect-smol/.changeset/pre/eff-701-httpapierror-respondable.md,全文只有 frontmatter 与一句变更描述:
--- "effect": patch --- Make all built-in `HttpApiError` classes implement `HttpServerRespondable`, so they can be returned directly from plain HTTP server handlers outside of `HttpApi`.三个要点值得注意:
- 变更级别是
patch:这是补丁级变更,说明它被视为行为增强而非破坏性修改——之前这些错误类在HttpApi体系内照常工作,之后它们"额外获得"了被普通服务端识别的能力; - 位置在
.changeset/pre/目录:changesets 工具在预发布(pre-release)模式下会把变更记录移入pre/子目录。结合 .repos/effect-smol/packages/effect/package.json 中version为4.0.0-rc.112可以确认,该变更属于effect包4.0.0预发布周期内的迭代。t3code 主仓库通过 patches/effect@4.0.0-rc.112.patch 锁定的正是同一版本线,说明本仓库消费的 Effect 运行时与这份变更记录来自同一发布序列; - 变更目标是
HttpApiError内置错误类:即400 Bad Request、404 Not Found这类由框架统一提供的状态码错误,而不是用户自定义错误。
内置错误类全景:13 个错误类与状态码映射
变更后落地的实现位于 .repos/effect-smol/packages/effect/src/unstable/httpapi/HttpApiError.ts。该模块导出的内置错误类与 HTTP 状态码的完整对应关系如下,均可在源文件中直接验证:
| 错误类 | 状态码 | 说明 | ErrorReporter.ignore |
|---|---|---|---|
BadRequest | 400 | 请求错误 | 是 |
Unauthorized | 401 | 未认证 | 是 |
Forbidden | 403 | 无权限 | 是 |
NotFound | 404 | 资源不存在 | 是 |
MethodNotAllowed | 405 | 方法不允许 | 是 |
NotAcceptable | 406 | 不可接受 | 是 |
RequestTimeout | 408 | 请求超时 | 是 |
Conflict | 409 | 冲突 | 是 |
Gone | 410 | 资源已移除 | 是 |
UnprocessableEntity | 422 | 语义错误 | 是 |
InternalServerError | 500 | 服务器内部错误 | 否 |
NotImplemented | 501 | 未实现 | 否 |
ServiceUnavailable | 503 | 服务不可用 | 否 |
以NotFound为例,源码结构(见 HttpApiError.ts)是:
export class NotFound extends Schema.Error<NotFound>("effect/HttpApiError/NotFound")({ _tag: Schema.tag("NotFound") }, { description: "NotFound", httpApiStatus: 404 }) { override readonly [ErrorReporter.ignore] = true [HttpServerRespondable.symbol]() { return Effect.succeed(notFoundResponse) } }其中notFoundResponse是模块顶部预创建的HttpServerResponse.empty({ status: 404 })(HttpApiError.ts#L21-L33 为全部 13 个状态码各缓存了一个空响应实例,避免每次渲染都新建对象)。
从源码结构看,每个错误类承担了三重身份:
Schema.Error派生类:携带httpApiStatus元数据,可被 OpenAPI 生成、HttpApi端点/中间件的错误声明、反射与生成的客户端所理解——这是变更之前就存在的角色;Respondable协议实现者:通过实现[HttpServerRespondable.symbol]()方法,声明自己应当渲染为哪个响应。这是本次 changeset 新增的能力;- 错误报告策略:所有 4xx 类设置了
[ErrorReporter.ignore] = true(表示客户端错误不应触发全局错误上报),而InternalServerError、NotImplemented、ServiceUnavailable三个 5xx 类没有该标记,意味着直接返回 5xx 错误类时仍会被错误报告器记录。这一区分与运维直觉一致:客户端错误是常态,服务器错误才需要告警。
*NoContent模式变体
每个状态码错误类还配套导出了一个NoContent模式变体,例如NotFoundNoContent(HttpApiError.ts#L152-L154):
export const NotFoundNoContent = NotFound.pipe(HttpApiSchema.asNoContent({ decode: () => new NotFound({}) }))asNoContent变体把"空响应体"固化为契约的一部分:解码一个空响应会直接还原为对应错误值。这与Respondable协议方向正好互补——协议解决"服务端错误 → HTTP 响应"的编码方向,NoContent模式解决"HTTP 响应 → 错误值"的解码方向,两端都用同一个类型。
HttpApiSchemaError:请求/响应解包失败的专用错误
同一模块还定义了 HttpApiSchemaError,它不是状态码错误,而是由 HTTP API 运行时在请求组件解码失败时抛出的错误:
export class HttpApiSchemaError extends Data.TaggedClass("HttpApiSchemaError")<{ readonly kind: "Params" | "Headers" | "Query" | "Body" | "Payload" | "ResponseHeaders" readonly cause: Schema.SchemaError }> { ... [HttpServerRespondable.symbol]() { return Effect.succeed(badRequestResponse) } }其kind字段记录失败发生的组件位置(路径参数、请求头、查询串、请求体、负载或响应头),cause保存原始SchemaError;它提供static is()类型守卫与static wrap()帮助方法用于把SchemaError提升为带位置的包装错误。渲染上它固定为空的400 Bad Request。该错误类同样实现了Respondable协议,与 changeset 中"all built-in HttpApiError classes"的表述一致。
协议机制:HttpServerRespondable如何被运行时识别
协议本体定义在 .repos/effect-smol/packages/effect/src/unstable/http/HttpServerRespondable.ts,核心是一个字符串符号:
export const symbol = "~effect/http/HttpServerRespondable" export interface Respondable { [symbol](): Effect.Effect<HttpServerResponse, unknown> }任何对象只要在自身携带该符号属性(即实现[symbol]()方法),就被视为"可响应"值。模块提供三个转换函数,构成完整的识别与降级逻辑:
toResponse:严格转换
toResponse(self)先检查值是否已经是HttpServerResponse(是则直接成功返回),否则调用self[symbol]()并Effect.orDie包裹——即协议方法内部抛错会转成 defect。适合在类型系统已经保证值可响应时使用。
toResponseOrElse:带兜底的宽松转换
export const toResponseOrElse = (u: unknown, orElse: HttpServerResponse): Effect.Effect<HttpServerResponse> => { if (Response.isHttpServerResponse(u)) { return Effect.succeed(u) } else if (isRespondable(u)) { return Effect.catchCause(u[symbol](), () => Effect.succeed(orElse)) } else if (Schema.isSchemaError(u)) { return Effect.succeed(badRequest) } else if (Cause.isNoSuchElementError(u)) { return Effect.succeed(notFound) } return Effect.succeed(orElse) }识别顺序是:已构造好的响应 >Respondable协议值(含变更后加入的全部HttpApiError类)>SchemaError(渲染为 400)>NoSuchElementError(渲染为 404)> 用户传入的兜底响应。协议值渲染失败时通过Effect.catchCause回退到orElse,保证任何情况下都有响应可发。
toResponseOrElseDefect:defect 通道的保守转换
处理 defect(非预期崩溃)时只识别响应值与Respondable值两类,其余一律回退到orElse——不猜测 defect 应该映射到什么状态码,符合"未预期错误保守处理"的原则。
实际调用链:普通 handler 的错误为何能被自动渲染
这条能力在普通 HTTP 服务端的落点是错误中间件。.repos/effect-smol/packages/effect/src/unstable/http/HttpServerError.ts 中的处理逻辑展示了运行时如何消费该协议:
// 类型化错误通道 effect = Respondable.toResponseOrElse(reason.error, internalServerError) // defect 通道 effect = Respondable.toResponseOrElseDefect(reason.defect, internalServerError)两处都把InternalServerError空响应作为兜底参数传入。结合toResponseOrElse的分支逻辑,可以还原出普通 handler 中抛错的完整决策表:
| handler 中产生的值 | 最终响应 |
|---|---|
HttpServerResponse实例 | 原样发送 |
NotFound等任一内置HttpApiError值 | 该错误的空状态码响应(如 404) |
HttpApiSchemaError | 空 400 |
SchemaError(裸的模式错误) | 空 400 |
NoSuchElementError | 空 404 |
| 其他类型化错误 / defect | 兜底 500 |
这正是 changeset 描述"returned directly from plain HTTP server handlers outside ofHttpApi"的运行时含义:在HttpApp/HttpServer这类普通路由服务中,业务代码把new NotFound({})作为错误抛出或返回,错误中间件就会经由toResponseOrElse将其渲染为 400/404 等对应响应,无需先手工构造HttpServerResponse,也无需HttpApi的端点声明机制参与。
变更的实际影响与适用前提
从本次变更记录与配套源码可以归纳出三点实际影响:
- 普通服务端的错误表达成本下降。此前在
HttpApi之外的普通 handler 中,要让某个状态码返回给客户端,通常需要显式构造HttpServerResponse.empty({ status: 404 })之类的响应对象或自定义携带渲染逻辑的错误类型。变更后,13 个内置状态码错误类加HttpApiSchemaError均可直接作为错误值使用,由中间件统一渲染; - 错误语义与上报语义保持一致。4xx 错误类携带
ErrorReporter.ignore标记,直接抛Unauthorized不会触发全局错误报告,而抛InternalServerError会——即使你是在普通 handler 里"正常地"返回 5xx; - 协议面向扩展开放。
Respondable是一个开放接口,任何自定义服务端错误都可以通过实现[HttpServerRespondable.symbol]()获得同样的"直接返回即渲染"能力,HttpApiError内置类只是该协议的首批内置实现者。
适用前提需要说明:这些模块位于effect包的unstable/http与unstable/httpapi子模块下(源文件路径为packages/effect/src/unstable/http...),对应effect@4.0.0-rc.112预发布版本;@since标注均为4.0.0,即该能力以 4.0 稳定版为基线。t3code 主仓库通过 patches/effect@4.0.0-rc.112.patch 与 patches/@effect__vitest@4.0.0-rc.112.patch 锁定同一版本线,引用本仓库时以.repos/effect-smol内实际源码为准。
关键路径索引
| 文件 | 内容 |
|---|---|
| .repos/effect-smol/.changeset/pre/eff-701-httpapierror-respondable.md | 本文所依据的 changeset 变更记录 |
| .repos/effect-smol/packages/effect/src/unstable/httpapi/HttpApiError.ts | 13 个内置状态码错误类、*NoContent变体与HttpApiSchemaError的实现 |
| .repos/effect-smol/packages/effect/src/unstable/http/HttpServerRespondable.ts | Respondable协议符号、isRespondable守卫与三个转换函数 |
| .repos/effect-smol/packages/effect/src/unstable/http/HttpServerError.ts | 错误中间件消费协议的调用点(toResponseOrElse/toResponseOrElseDefect) |
| .repos/effect-smol/packages/effect/package.json | effect包版本4.0.0-rc.112,确认变更所属版本线 |
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考