BitzOrcas 把分页、列表、搜索、预览和统计统一建模为 RFC 10008 QUERY 请求。查询条件放在 JSON 正文中,不再挤进 URL;按稳定标识读取单一资源时才使用 GET。
1. 方法选择
前端开发者通常只调用类型化 API,但仍应知道每个方法表达的业务语义。方法选错会让缓存、审计、重试和代码生成器得到错误信号。
| 方法 | 应使用的场景 | 不应使用的场景 |
|---|---|---|
GET | 按稳定路由标识读取单一资源;至多带一个语言、格式或投影开关等简单标量 | 集合、分页、搜索、预览、统计、复杂对象、两个及以上非路由参数、请求正文 |
QUERY | 集合、分页、搜索、复杂筛选、预览、统计和只读校验 | 创建、更新、删除、发送通知等状态变更;普通 Output Cache |
POST | 创建资源、非幂等动作、提交处理;或作为 QUERY 的 /_query 兼容传输 | 普通只读查询的首选入口 |
PUT | 客户端已知资源 URI 的完整替换或幂等 upsert | 只改少数字段、非幂等动作 |
PATCH | 对既有资源做显式、受类型约束的部分变更 | 完整替换、通用无类型 JSON Patch |
DELETE | 按稳定路由标识删除或撤销 | 携带筛选正文批量查找资源 |
普通首方 JSON API 不保留集合 GET 别名。即使列表暂时没有筛选条件,也发送正文 {},这样以后增加条件时不必更换方法或新增并行契约。OAuth/OIDC、SCIM、浏览器导航、下载、Feed、Sitemap 和健康检查等协议入口按各自标准处理,不做机械迁移。
2. 线上契约
以下请求是当前用户列表查询的实际传输形态。QUERY 是安全、幂等方法;服务端 Query Handler 不得改变业务状态。
QUERY /api/users HTTP/1.1Host: api.example.comAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/jsonAccept: application/json
{ "keyword": "张三", "status": "Active", "pageIndex": 1, "pageSize": 20, "sortField": "displayName", "sortOrder": "asc"}如果当前浏览器、网关或代理链路不能传递 QUERY,SDK 使用同一份正文调用兼容入口:
POST /api/users/_query HTTP/1.1Host: api.example.comAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/jsonAccept: application/json
{ "keyword": "张三", "status": "Active", "pageIndex": 1, "pageSize": 20, "sortField": "displayName", "sortOrder": "asc"}两条入口共享 Query、Handler、授权、租户边界、响应 DTO、状态码和 Problem Details。它们只有 HTTP 方法与路径不同。服务端会在两种响应上写入:
Accept-Query: application/json请求必须同时满足以下约束:
Content-Type是application/json;- 正文非空,无筛选条件时发送
{}; - 路由 token 始终从 URL 绑定,并覆盖正文中的同名字段;
- 不生成
GET兼容入口; - 默认 Output Cache 不用于
QUERY,因为仅按 URL 建键会混淆不同正文。
正文不出现在 URL,不代表它天然保密。生产环境仍须使用 HTTPS,并在请求日志、APM 和网关采样中脱敏关键词、证件号及其他敏感筛选条件。
3. Platform SDK 怎样降级
createPlatformClient 在每个 client 实例中维护当前链路的 QUERY 能力状态。页面不参与判断,也不应捕获错误后自行补发 POST。
会触发降级的结果
| 结果 | SDK 行为 | 是否记住链路不支持 |
|---|---|---|
405 Method Not Allowed | 用 POST /_query 再试一次 | 是 |
501 Not Implemented | 用 POST /_query 再试一次 | 是 |
| Fetch 网络错误 | 用 POST /_query 再试一次 | POST 能收到任意 HTTP 响应时记住 |
浏览器把 CORS 预检拒绝、DNS 失败、连接拒绝等情况统一暴露为网络错误,JavaScript 无法可靠区分。因此网络错误会获得一次 POST 尝试。QUERY 必须保持安全、幂等:第一条请求即使已经到达服务端,再执行一次也不能产生业务副作用。
不会触发降级的结果
400、401、403、404、408、409、415、422、429,以及除 501 外的 5xx 业务或基础设施响应,都不会触发传输降级。特别是:
401仍走统一 access-token 刷新流程,刷新成功后重放同一种查询传输;403直接返回Forbidden,不能靠换成 POST 绕过授权;415表示请求没有提交合法 JSON 正文,应修复调用方;- 超时与主动取消不会补发 POST;
- 业务校验失败不会因为降级被重复执行或掩盖。
能力状态只存在于当前 PlatformClient 实例。刷新页面或重新创建 Provider 后会重新探测;它不是服务端 Feature,也不会写入浏览器持久存储。
4. OpenAPI 3.1 中看到的为什么是 POST
当前 .NET 10 Host 明确固定 OpenAPI 3.1。运行时 QUERY 入口从文档中隐藏,POST 降级操作承担标准 request/response schema,并附加五个扩展字段:
Scalar 的启用门禁、Server 选择、认证与反向代理配置见 OpenAPI 与 Scalar 文档面;这些运行时文档设置不会改变 QUERY/POST 协商规则。
{ "openapi": "3.1.1", "paths": { "/api/users/_query": { "post": { "x-http-query-method": "QUERY", "x-http-query-content-type": "application/json", "x-http-query-path": "/api/users", "x-http-query-fallback-method": "POST", "x-http-query-fallback-path": "/api/users/_query", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BitzOrcasIdentityApplicationIdentitySearchUsersSearchUsersQuery" } } } } } } }}这会带来一个有意保留的差异:
| 视角 | 方法与路径 |
|---|---|
| 应用代码的逻辑契约 | QUERY /api/users |
| 不支持 QUERY 的兼容传输 | POST /api/users/_query |
| OpenAPI 3.1 标准 Operation | paths['/api/users/_query'].post |
当前 generated.d.ts 类型索引 | paths['/api/users/_query']['post'] |
openapi-typescript 可以从 POST Operation 生成查询正文和响应类型,但不会替 Platform SDK 执行运行时协商。SDK 的模块 API 仍使用逻辑路径 /api/users 和方法 QUERY。第三方代码生成器如果忽略 x-http-query-*,应直接调用文档中可见的 POST 降级入口;不得把它猜成集合 GET。
5. 前端接入方式
页面只消费类型化 API:
// 页面只调用类型化 API,不感知 QUERY 与 POST 的传输选择。const result = await api.searchUsers({ keyword: filter.keyword, status: filter.status, roleId: filter.roleId, accountType: filter.accountType, pageIndex: 1, pageSize: 20, sortField: "displayName", sortOrder: "asc",});
if (!result.ok) { // 统一展示 Problem Details;页面不自行补发 POST。 showProblem(result.error); return;}
renderUsers(result.data.items);只有维护 @bitz/platform-sdk 模块 API 时才接触方法和路径。body 与 query 在 method: 'QUERY' 时都会作为 JSON 正文发送;现有模块方法以自己的契约写法为准,不要同时传两者。
searchUsers(query: UserQuery): Promise<PlatformResult<UserPage>> { // 逻辑路径保持资源路径,pipeline 负责按需追加 /_query。 return client.request<UserPage>('/api/users', { method: 'QUERY', // 查询条件进入 JSON 正文,不拼接到 URL。 body: { keyword: query.keyword ?? undefined, status: query.status ?? undefined, roleId: query.roleId ?? undefined, accountType: query.accountType ?? undefined, pageIndex: query.pageIndex, pageSize: query.pageSize, sortField: query.sortField ?? undefined, sortOrder: query.sortOrder ?? undefined, }, });}不要在页面中写 fetch,不要自己拼 /_query,也不要把 JSON 正文改回 URLSearchParams。这样才能复用认证刷新、租户上下文、超时、取消、Problem Details 和自动降级。
6. CORS、代理与网关
QUERY 不属于 CORS safelisted method。浏览器跨源调用时一定先发送 OPTIONS 预检。生产配置至少要允许 QUERY、POST 降级所需的 POST、预检使用的 OPTIONS,以及 JSON 与认证请求头:
{ "Cors": { "AllowedOrigins": ["https://console.example.com"], "AllowAnyOrigin": false, "AllowCredentials": true, "AllowedMethods": [ "GET", "QUERY", "POST", "PUT", "PATCH", "DELETE", "OPTIONS" ], "AllowedHeaders": [ "Content-Type", "Authorization", "X-Client-Platform", "traceparent", "correlationId", "Idempotency-Key" ] }}API Host 已把 Accept-Query 加入 CORS exposed headers,因此浏览器客户端能够读取该响应头。边缘层还要逐项核对:
- CDN、WAF、Ingress、OpenResty 和 API Gateway 是否允许未知 HTTP 方法;
- 方法白名单是否同时包含
QUERY、POST和OPTIONS; - 路由规则是否保留
/_query后缀; - 请求正文大小限制是否同时作用于 QUERY 与 POST;
- 限流、审计和指标是否把两条传输聚合为同一个逻辑只读操作;
- 开发期 Vite proxy 是否原样转发 QUERY,而不是提前改写方法。
POST 降级入口在网络设备眼中仍是 POST。网关策略应按路径和端点元数据把 POST */_query 视为只读兼容入口,不能把它计入“资源创建成功率”,也不要仅因传输方法是 POST 就套用写命令专属的幂等键或审计分类。Cookie 场景的 Origin 与 CSRF 防护仍按 Host 安全策略执行。
7. 缓存、重试与可观测性
RFC 10008 允许缓存 QUERY 响应,但缓存键必须考虑请求内容及影响结果的请求元数据。BitzOrcas 当前默认 Output Cache 只适合 URL 可完整标识的读取,因此生成器用 BZEP005 阻止 QUERY 配置普通 Output Cache。前端也不应假定浏览器或 CDN 会缓存 QUERY。
监控时应同时保留“逻辑方法”和“实际传输”:
| 字段 | 示例 | 用途 |
|---|---|---|
| 逻辑方法 | QUERY | 业务查询量、成功率、授权与延迟 |
| 实际方法 | QUERY 或 POST | 判断链路兼容性 |
| 逻辑路径 | /api/users | 聚合同一用例 |
| 实际路径 | /api/users 或 /api/users/_query | 定位网关与路由问题 |
如果 POST 降级比例突然升高,先检查近期代理、CORS 和 WAF 配置,不要把它误判为业务端点退化。
8. .NET 11 与 OpenAPI 3.2 之后会怎样
OpenAPI 3.2 已正式增加 Path Item 的 query 字段。ASP.NET Core 自 .NET 11 Preview 6 起可以把 MapMethods(..., ["QUERY"], ...) 生成为原生 QUERY Operation,并把 OpenAPI 默认版本改为 3.2。
这不表示现在应在生产仓库启用预览运行时,也不表示以后升级 Target Framework 就能自动完成迁移。切换契约前,API Host、Microsoft.AspNetCore.OpenApi、Microsoft.OpenApi、Scalar、OpenAPI 校验器、openapi-typescript、网关和消费方代码生成器都要通过 3.2 兼容验证。
迁移后的标准形态应为:
{ "openapi": "3.2.0", "paths": { "/api/users": { "query": { "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BitzOrcasIdentityApplicationIdentitySearchUsersSearchUsersQuery" } } } } } } }}预期接口变化
| 项目 | 当前 .NET 10 / OpenAPI 3.1 | 未来 .NET 11 / OpenAPI 3.2 |
|---|---|---|
| 首选运行时入口 | QUERY /api/users | 不变 |
| POST 兼容入口 | POST /api/users/_query | 先保留,是否删除另行版本化 |
| 标准 OpenAPI Operation | POST /api/users/_query | QUERY /api/users |
| QUERY 描述 | x-http-query-* | 标准 query 字段;扩展进入过渡或删除 |
| 生成类型索引 | paths['/api/users/_query']['post'] | 预计转为 paths['/api/users']['query'] |
| 页面调用 | api.searchUsers(...) | 不变 |
| 浏览器自动降级 | SDK 负责 | 仍由 SDK 负责 |
OpenAPI 3.2 解决的是“契约能够标准描述 QUERY”,不会让旧浏览器、代理或 WAF 自动支持这个方法。因此 POST 降级不能在切换 3.2 的同一版本里直接删除。先保留兼容入口并收集实际降级率;删除它应作为单独的破坏性变更,给出版本、遥测证据和消费方迁移期。
升级门禁
准备切换时逐项确认:
- .NET 11 已进入仓库允许的稳定发布通道,不再依赖 Preview SDK;
- OpenAPI artifact 的根版本为
3.2.0,主路径出现标准queryOperation; - QUERY Operation 保留原 request body、响应、认证、Tag、摘要、错误模型和稳定 operationId;
- POST
/_query使用独立 operationId,并明确标为兼容入口,避免代码生成名称冲突; - Scalar、OpenAPI lint、契约差异工具和
openapi-typescript不会丢弃query; generated.d.ts的路径索引迁移已由契约别名吸收,页面没有直接引用 transport path;- 浏览器分别通过原生 QUERY、强制 405、强制 501 和预检拒绝四条路径;
- CORS、CDN、WAF、Ingress 和 API Gateway 的方法白名单已在真实入口验证;
401、403、415、422和429仍不会触发 POST 降级;- 没有重新引入集合 GET,也没有在请求正文感知缓存完成前解除
BZEP005。
如果 .NET 11 已升级但下游工具还不能消费 3.2,应继续显式固定 OpenAPI 3.1。ASP.NET Core 11 在旧版本文档中可能把 QUERY 放到 x-oai-additionalOperations;这仍是扩展表示,不等于本项目已经完成 3.2 契约迁移。
9. 验证调用
下面两条命令应返回相同业务结果。ACCESS_TOKEN 来自正常登录流程,API 是待验证的真实入口:
# 通过真实入口验证首选 QUERY;不要只测直连 Host。API=https://api.example.com
curl -i -X QUERY "$API/api/users" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ --data '{"pageIndex":1,"pageSize":20}'
# 降级入口必须使用与 QUERY 完全相同的 JSON 正文。curl -i -X POST "$API/api/users/_query" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ --data '{"pageIndex":1,"pageSize":20}'跨源部署还要直接验证预检:
# 预检应覆盖 QUERY,而不是只验证常见的 POST。curl -i -X OPTIONS "$API/api/users" \ -H 'Origin: https://console.example.com' \ -H 'Access-Control-Request-Method: QUERY' \ -H 'Access-Control-Request-Headers: authorization,content-type,x-client-platform'预检响应必须允许配置中的 Origin、QUERY 与请求头。仅用 Postman 或服务端测试客户端无法证明浏览器 CORS 可用。
10. 源码与规范
当前行为可从后端单仓根目录核对:
# 后端:确认生成路由、协议元数据和契约测试仍在。rg -n "HttpRoute.Query|HttpQueryProtocol|x-http-query|_query" \ src/Framework src/Platform src/Modules tests
# 前端:确认逻辑 QUERY 与自动降级仍集中在 Platform SDK。rg -n "method: 'QUERY'|appendHttpQueryFallbackPath|shouldFallbackHttpQuery" \ frontend/packages/platform-sdk/src
# Artifact:当前稳定输出应保持 OpenAPI 3.1 与 QUERY 扩展字段。rg -n '"openapi": "3\.1|x-http-query-' artifacts/openapi/openapi-v1.json协议依据与升级资料:
- RFC 10008:The HTTP QUERY Method
- OpenAPI 3.2 Path Item Object
- ASP.NET Core 11 中的 HTTP QUERY 与 OpenAPI 3.2
- ASP.NET Core 11 的 OpenAPI 3.2 默认版本变更
返回前端 · 平台 SDK · CORS 与响应头 · Web / API 构建块