Skip to content

@xapiforge/core — 场景清单(Scenarios) ​

这份文档是库设计/测试/文档的 single source of truth。

设计哲学:库设计应该覆盖所有这些场景。如果某个场景库没处理,标记 ⚠️ 或 ❌ 并在 BEHAVIOR-CONTRACT.md 同步。

v0.2 架构:库 = HttpClient + AuthService + createNet + middleware 链。 envelope 解包 / 业务错误判定归业务侧 middleware(如 createAbpUnwrapMw)。 库提供 Middleware 类型 + 内置 auth-mw / retry-mw / http-error-mw。

如何使用本表:

  1. 设计新特性:从表里找相关场景,加期待行为
  2. 写测试:每个场景一个 case
  3. 改行为:先改本表,再改代码
  4. 发现新场景:加到表里

覆盖范围:不限于ABP (.NET) projects。要枚举通用后端 + 典型边角 + 未来可能。

版本:当前实现(2026-08-04,data-or-throw + 结构化 ApiError)


分类索引 ​

Layer范围场景数v0.2 覆盖
A通用后端协议(典型 + 已遇到)22✅ 22/22
B网络层 / 边角情形28✅ 24/28(4 流式 v0.3)
C业务 envelope 变体10✅ 10/10
D鉴权 / token 场景18✅ 18/18
E生命周期 / 资源管理11✅ 11/11
F错误传播 / Promise 行为8✅ 8/8
总计~97✅ 93/97(96%)

场景编号规则:{Layer}{序号} — 例如 A1 = Layer A 第 1 个场景


Layer A:通用后端协议 ​

关键场景:ABP + Spring Boot + OAuth2 + RFC 7807 + GraphQL + 微信/钉钉 + 支付宝

v0.2 库的承诺:库不预设 envelope 形状、不探测字段名。{ code, data, msg } 一族由工程用官方 createEnvelopeMiddleware 显式配字段名;其余业务侧写 middleware(如 createAbpUnwrapMw)声明。 库内置 auth-mw(401 → refresh → retry)+ retry-mw(429 退避)+ http-error-mw(最终 validateStatus 校验 → ApiError)。

#后端 / 协议典型场景响应形态库行为
A1ABP (.NET)ABP (.NET) projects{code, data, msg} 三段库默认不感知,业务侧 guard
A2Spring Boot / Spring Security银行 / 大型国企{code, message, data} 或 {status, message}业务侧 guard
A3OAuth2 / OIDC 标准错误 (RFC 6749)任何标准 OAuth server{error, error_description, error_uri}业务侧 guard;token endpoint 用 AuthApi 自己 fetch
A4REST 标准错误 (RFC 7807 Problem Details)Spring 5+ / 新项目{type, title, status, detail, instance}业务侧 guard
A5企业微信 / 钉钉 / 飞书 API国内中台 / OA{errcode, errmsg} 或 {code, message, data}业务侧 guard
A6微信小程序 errcode微信生态{errcode, errmsg} HTTP 200业务 200 鉴权码场景(已在 mock 演示)
A7支付宝 / 银联支付{code, msg, sub_code, sub_msg}业务侧 guard
A8GraphQL (Apollo / Yoga)新项目 / BFF{data, errors[]} HTTP 200业务侧 guard(鉴权可能在 errors[].extensions.code)
A9gRPC-Web新微服务{} 成功 + {code, message} 失败业务侧 guard
A10JSON-RPC 2.0区块链 / RPC{jsonrpc: "2.0", result} 或 {jsonrpc, error: {code, message}}业务侧 guard
A11HAL / JSON:APIREST 严格规范{data, links, meta} 或 {data, included}业务侧 guard
A12嵌套 envelope {status, result}自研 API{status: "ok", result: {...}}业务侧 guard(鉴权码可能在 result.code)
A13array root (无 envelope)简单 REST[{...}, {...}] 或裸 {...}业务侧 guard
A14业务 200 + 鉴权码国内中台{code: 40001, msg: "token expired"} HTTP 200关键:业务侧 middleware 抛 RetryableError({needsRefresh:true}) → 库 refresh + 重试 ✅ 实测
A15业务 200 + success bool旧 PHP / 国内老项目{success: true, data} 或 {ok: true, data}业务侧 guard
A16HTTP 401 / 403 / 419 / 440 / 498 / 499各种鉴权失败空 body 或 envelope401 先 refresh;最终失败及其他状态默认抛 ApiError,可用 validateStatus 覆盖
A17HTTP 429 + Retry-After header限流envelope + Retry-After: 60自动退避重试;耗尽后抛 ApiError(kind="rate-limit")
A18HTTP 5xx + HTMLNginx / Tomcat 默认错误页<html>...抛 ApiError(kind="http"),字符串 body 与原始 Response 均保留
A19HTTP 5xx + JSON envelope现代 .NET / Java{code: 500, msg: "..."}抛 ApiError(kind="http"),body 为解析后的 JSON
A20HTTP 503 + JSON body (k8s 维护中)微服务{message: "Service Unavailable"}同 A19;可由全局/单请求 validateStatus 改为成功
A21空响应 204 No ContentABP / 部分 endpoint""envelope=null,bodyText="";guard 决定
A22空响应 200 + 空 body部分 GET""同上

Layer B:网络层 / 边角情形 ​

当前库的承诺:fetch 失败、超时和主动取消抛 NetworkError,并分别使用 network / timeout / abort kind。 HTTP 状态失败与网络失败分开:前者抛 ApiError(kind="http");429 内部重试耗尽后抛 ApiError(kind="rate-limit")。

#情形库行为
B1CORS preflight 失败 (OPTIONS 返 4xx)fetch 抛错 → NetworkError
B2HTTPS 证书过期浏览器拒绝,NetworkError
B3DNS 解析失败fetch 抛错 → NetworkError
B4TCP 连接超时fetch AbortController 超时 → NetworkError
B5服务端主动 close (ECONNRESET)fetch 抛错 → NetworkError
B6服务端关闭 keep-alivefetch 抛错 → NetworkError
B7响应 body 超大 (> 100MB)浏览器 OOM / 业务侧超时
B8gzip / br 解压失败fetch 内部错误 → NetworkError
B9chunked transfer 编码异常fetch 内部错误 → NetworkError
B10响应 Content-Type 与 body 不匹配guard-helpers.ts:parseJson 解析失败 → envelope=null
B11响应含 BOM (\uFEFF)JSON.parse 通常容忍,业务侧可处理
B12响应含 surrogate pair (emoji)UTF-8 标准,业务侧处理
B13响应 body 是 NDJSON (流式)库不支持,业务侧用 SSE/WebSocket
B14响应是 Server-Sent Events库不支持
B15请求 body 含二进制 (file upload)已支持(FormData)
B16请求 body 含 URL-encoded form已支持(URLSearchParams)
B17请求 body 是 stream库不支持
B18请求含 cookie业务侧手动塞 header
B19请求是 multipart 混合 text + file已支持(FormData)
B20CORS preflight OPTIONS浏览器自动发,业务侧不管
B21响应 Set-Cookie业务侧 token store 处理
B22SameSite cookie 跨域业务侧配置
B23重定向 3xxfetch 默认 follow,库不管
B24重定向循环fetch 默认最多 20 次 follow,错误向上
B25响应 WebSocket Upgrade库不支持
B26HTTP/2 pushfetch 内部处理
B27代理 407 Proxy Authentication Requiredfetch 抛错
B28业务侧 guard 抛错库向上传,⚠️ 待确认

Layer C:业务 envelope 变体(unhappy path) ​

v0.2 库的承诺:envelope 在 middleware 的 onResponse 阶段解包。C1–C8 这类「顶层 code + data」变体官方 createEnvelopeMiddleware 按字段名配置即可覆盖(ok 支持列表与布尔,严格相等);C9 数组根、C10 多层嵌套由业务侧自写。 库提供 Middleware 类型 + HttpResponse(含 bodyText / envelope / data)。

#envelope 形态例子业务侧实现
C1data 字段是 string{code: 0, data: "https://file.url"}unwrap: p.code === 0 ? p.data : p
C2data 字段是 array root{code: 0, data: [{...}, {...}]}同上
C3data 字段是分页 {items, totalCount}{code: 0, data: {items: [...], totalCount: 100}} (ABP PagedResultDto)同上 + 业务侧 unwrapAbpPaged<T>
C4data 字段是 null{code: 0, data: null} (DELETE)同上
C5msg 字段缺失{code: 0, data: ...} 老项目同上
C6code 字段类型是 string{code: "0", data: ...} (PHP 老项目)typeof p.code === "string" 兼容
C7code 字段是 boolean{success: true, data: ...}typeof p.success === "boolean"
C8响应里同时含 code 和 status{code: 0, status: "ok", data: ...}业务侧选一
C9envelope 是数组而非 object[{...}, {...}]unwrap: p => Array.isArray(p) ? p : []
C10envelope 嵌套 2 层以上{result: {inner: {code, data}}}unwrap 链式:p => p.result.inner.code === 0 ? p.result.inner.data : p

Layer D:鉴权 / token 场景 ​

v0.2 库的承诺:库提供 AuthService(单飞 + 窗口保护 + 超时)+ auth-mw(401 → refresh → retry)。 鉴权判定:HTTP 401 由 auth-mw 处理;业务 200 鉴权码由业务侧 middleware 抛 AuthError 触发 refresh。

#情形库行为
D1token 即将过期 (业务主动提前 refresh)库不感知;业务侧定时器触发
D2refresh token 也过期 (refresh 自己返 401)refresh 抛错 → AuthError + onAuthFailed
D3refresh 端点限流 (429)refresh 抛错 → AuthError + onAuthFailed
D4refresh 端点返 5xxrefresh 抛错 → AuthError
D5refresh 端点返业务 envelope 鉴权码refresh 抛错 → AuthError(业务侧 authRefresher.refresh() 自己解析)
D6refresh 端点 body 非 JSON业务侧 authRefresher.refresh() 自己处理
D7refresh 端点 timeoutwithTimeout 抛 NetworkError → 转 AuthError
D8token 含特殊字符 (+ / =)业务侧自己 encode;库透传 header
D9token 是 JWT 格式 (含 exp claim)业务侧 authRefresher.refresh() 可解析 JWT 提前 refresh
D10token 是 opaque (UUID)业务侧不管,库透传
D11双 token 体系 (access + refresh)业务侧 authRefresher.refresh() 返 access token;库只关心 access
D12单 token 体系 (长期不过期)业务侧 refresh 自己实现
D13多 tenant (ABP __tenant header)业务侧 per-request header;库不管
D14OAuth2 PKCE 流程业务侧自己处理 code_verifier;库不管
D15OAuth2 client_credentials业务侧 authRefresher.refresh() 自己实现
D16SAML 断言业务侧自己处理;库不管
D17API Key header业务侧每个请求塞 X-API-Key;库不管
D18HMAC 签名 (AWS SigV4 / 阿里云)业务侧每次请求签名;库不管

Layer E:生命周期 / 资源管理 ​

#情形库行为
E1createNet 重复调用✅ 工厂模式:每次返独立 client,无全局状态
E2createNet 后 tokenStore 引用变化✅ 工厂模式:client 持有 authService 引用,不变
E3createNet 后 baseURL 变化✅ 工厂模式:client 独立,需重新 createNet
E4业务页面卸载时 fetch 未完成业务侧 AbortController 主动 abort
E5业务页面卸载后 fetch 完成 (late response)库不管;业务侧 catch 后判断是否更新 state
E6浏览器 tab 切换到后台 fetch 是否继续浏览器行为;库不管
E7Service Worker 拦截 fetch业务侧配置 SW;库不管
E8浏览器离线 → 在线后重试业务侧自己实现重试逻辑
E9同一 endpoint 不同业务实例调用库不管;业务侧自己管理
E10多 tab 共享 token (localStorage)库不管;业务侧用 storage event 同步
E11多 tab 互斥 logout库不管;业务侧用 storage event

Layer F:错误传播 / Promise 行为 ​

#情形库行为
F1业务侧 catch 后 throw 一个新错误库不管;透传
F2业务侧 catch 后 await 其他 promise库不管
F3业务侧 try/catch/finally 中 finally 抛错库不管
F4业务侧 await Promise.all([net.request(...), ...])任一 reject → 整个 reject
F5业务侧 Promise.allSettled 中部分请求失败各自 catch;库不影响
F6业务侧 Promise.race 中第一个请求失败race reject;其他继续
F7AbortController.abort() 在 fetch 中途触发库支持(opts.signal);抛 NetworkError(kind="abort")
F8用户主动取消请求同 F7

v0.2 架构覆盖性验证(2026-07-31) ​

系统性核对 97 个场景对 v0.2 架构(HttpClient + AuthService + createNet + middleware 链)的覆盖情况。

✅ 完全覆盖(93/97) ​

Layer覆盖关键机制
A (22)22/22业务侧 middleware 解 envelope;auth-mw 处理 401;retry-mw 处理 429;http-error-mw 统一校验最终状态
B (28)24/28HttpClient catch fetch 错误 → NetworkError;AbortController 超时/取消;FormData/URLSearchParams 支持
C (10)10/10业务侧 middleware onResponse 解包(string/array/paged/null/nested 均可)
D (18)18/18AuthService 单飞 + 窗口保护 + 超时;auth-mw 401→refresh→retry;业务侧 middleware 抛 RetryableError({needsRefresh:true}) 触发 refresh
E (11)11/11工厂模式无全局状态;AbortController 取消;业务侧 storage event 多 tab 同步
F (8)8/8请求失败统一 throw;未知运行错误归一化为 unexpected 并保留 cause;Promise 组合保持标准行为

❌ 不支持(4/97)— 流式响应,暂不实装 ​

详细设计见 stream-design.md(暂不实装,等真实业务场景驱动)

场景原因计划
B13 NDJSONHttpClient 一次性读 body暂不实装;真要时 responseType: "stream" + readNdjson helper
B14 SSE同上暂不实装;真要时 createSseClient(stream) helper
B17 stream bodyfetch 尚未广泛支持暂不实装;真要时 body: ReadableStream 直接传 fetch
B25 WebSocket协议不同不扩展(库定位 HTTP,业务侧用原生 WebSocket + AuthService)

⚠️ 设计如此(库不管) ​

场景原因
D1 token 即将过期业务侧定时器主动 refresh(库不感知 JWT exp)
E6 tab 后台 fetch浏览器行为
E7 Service Worker业务侧配置
E8 离线→在线重试业务侧自己实现

关键验证点 ​

  1. A14 业务 200 鉴权码(最关键):业务侧 middleware 检测 code=40001 → 抛 RetryableError({ needsRefresh: true }) → createNet retry loop catch → authService.refreshIfNeeded() → 重发(新 token 由 auth-mw onRequest 注入)。✅ 链路完整
  2. A17 429 + Retry-After:retry-mw 抛 RetryableError → createNet retry loop 读取 Retry-After 退避。✅ 库内置
  3. A18 5xx + HTML:最终状态校验抛 ApiError(kind="http"),解析后的字符串 body 和 Response 均保留。✅ 库内置
  4. D2 refresh token 也过期:AuthService refresh 抛错 → onAuthFailed() 回调 → 业务侧跳登录。✅ 链路完整
  5. E1-E3 工厂模式:createNet() 每次返独立 client,无全局状态污染。✅ v0.2 核心改进

✅ A14 实测(Playwright,2026-07-31) ​

测试脚本 /tmp/test-a14.py,链路完整通过:

text
[OK] 登录成功
[OK] 基本 GET 列表正常 (status=200)
[INFO] token (正常): mock-token...
[INFO] 已设置 token=biz-code-expired
[INFO] A14 调用结果: {'ok': True, 'status': 200, 'data': {'ok': True, 'checked': 'biz-code-expired path'}}
[OK] A14 链路成功!token 已被 refresh 更新: refreshed-token...
[OK] 0 console errors

完整链路:

  1. localStorage.app_token = "biz-code-expired" → 下次请求 auth-mw 注入 Authorization: Bearer biz-code-expired
  2. mock-server /api/auth/biz-code-expired 返 200 + {code: 40001, ...}
  3. abp-unwrap-mw.ts 检测 code=40001 ∈ AUTH_CODES → 抛 RetryableError({needsRefresh: true})
  4. create-net.ts retry loop catch → authService.refreshIfNeeded() → token 更新为 refreshed-token
  5. auth-mw onRequest 用新 token 重发 → mock 返 200 + {code: 0, data: {...}}
  6. abp-unwrap-mw.ts 解 envelope → 返 data → 业务代码拿到 {ok: true, checked: "biz-code-expired path"}

✅ Node 单测脚本实测(2026-07-31) ​

测试脚本 scripts/test/scenarios.ts,直接 import @xapiforge/core,覆盖 10 个 Section 共 259 个断言,全部通过:

text
xapiforge 场景验证开始
target: http://localhost:8787

━━━ Section 1: HttpClient(HTTP 层)━━━           ← A18/A19/A20/A21/A22/B4/B10/B11/B23/B16/F7
━━━ Section 2: middleware 单元测试 ━━━             ← retry-mw/http-error-mw/auth-mw
━━━ Section 3: AuthService 单元测试 ━━━            ← E1/单飞/窗口/D2/D7
━━━ Section 4: 业务 envelope 变体(C1-C10)━━━     ← C1-C10 + L4.8
━━━ Section 5: middleware 链 + retry loop ━━━      ← 429 重试计数 / 5xx → ApiError
━━━ Section 6: A14 业务 200 鉴权码(库层)━━━      ← A14 needsRefresh 完整链路
━━━ Section 7: 端到端鉴权(A16/D2/D7)+ B19 ━━━   ← 真实 /connect/token + 文件下载
━━━ Section 8: client.request 走内核(无 schema)━━━ ← data-or-throw / validateStatus
━━━ Section 9: 文件下载走内核 ━━━                  ← progress / 文件名 / auth / HTTP 错误

✅ Passed: 259
❌ Failed: 0

覆盖的关键场景:

场景验证方式结果
A14createNet + abpUnwrapMw + 真实 mock-serverneedsRefresh → refresh → retry → 200 ✅
A16createNet + auth-mw + token=expired → 401refresh → refreshed-token → retry → 200 ✅
A17retry-mw 429 + Retry-After → RetryableErrorcreateNet retry 3 次 → ApiError(kind="rate-limit") ✅
A18HttpClient 5xx + HTMLbody 原样保留,不抛错(middleware 决定)✅
A21/A22HttpClient 204 / 200+emptybodyText="" ✅
B4HttpClient timeoutMs=500, mock slow 2000msNetworkError("timeout") ✅
B11HttpClient BOM 响应JSON.parse 容忍 BOM ✅
B16HttpClient URLSearchParams body/connect/token 拿到 access_token ✅
B19HttpClient responseType:"blob"blob.size=1024 + Content-Disposition ✅
B23HttpClient 302 重定向fetch follow → 最终 401 ✅
C1-C1010 种 envelope 变体 unwrap 函数全部正确解包 ✅
D2createNet + auth-mw + refresh-401 scenarioAuthError + onAuthFailed(1) ✅
D7createNet + auth-mw + refresh-slow scenariorefreshTimeoutMs=500 → AuthError ✅
E1两个 AuthService 独立token 互不干扰 ✅
F7HttpClient signal.abort()NetworkError("aborted") ✅
HTTP 错误createNet 默认请求 404ApiError(kind="http"),body / Response 保留 ✅
状态覆盖全局与单请求 validateStatus 接受 404返回 { data, response },响应头可读 ✅
解析错误2xx + 非法 JSONApiError(kind="parse") + cause ✅
未知错误middleware 抛普通运行错误ApiError(kind="unexpected") + cause ✅
单飞10 并发 refreshIfNeededrefresher 调 1 次 ✅
窗口30s 内重复 refreshIfNeededrefresher 调 1 次(窗口内不重复)✅

库行为未覆盖场景(设计层面) ​

这些场景当前库不直接处理,业务侧需要自己实现。如有必要,库可扩展。

场景当前行为建议
A14 业务 200 鉴权码业务侧 middleware 抛 RetryableError({needsRefresh:true}) → 库 refresh + 重试✅ 实测通过(Playwright)
A17 限流 429 + Retry-Afterretry-mw 内置✅ 已支持
D1 token 即将过期业务侧定时器可加 createProactiveRefresh
B13/B14 流式响应库不支持v0.3 加 stream
F8 用户主动取消HttpClient opts.signal✅ 已支持

相关文档 ​


如何加新场景 ​

  1. 在合适的 Layer 段尾加一行表格
  2. 在 BEHAVIOR-CONTRACT.md 加对应编号
  3. 在 BACKEND-PROTOCOLS.md 加对应 JSON 示例(如果有新 envelope 形态)
  4. 评估库要不要支持;如果支持,加到 NetConfig 或库 API

MIT Licensed