Skip to content
bitzorcas
中EN

Reference

HTTP QUERY 与 POST 自动降级

说明 BitzOrcas 如何使用 RFC 10008 QUERY 承载分页、列表和复杂只读请求,以及 Platform SDK、OpenAPI 3.1、CORS、网关和未来 .NET 11 / OpenAPI 3.2 的兼容边界。

Last updated

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.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9
Content-Type: application/json
Accept: application/json
{
"keyword": "张三",
"status": "Active",
"pageIndex": 1,
"pageSize": 20,
"sortField": "displayName",
"sortOrder": "asc"
}

如果当前浏览器、网关或代理链路不能传递 QUERY,SDK 使用同一份正文调用兼容入口:

兼容入口
POST /api/users/_query HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9
Content-Type: application/json
Accept: 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。

是否正常 HTTP 响应405 或 501浏览器或代理网络拒绝POST 收到 HTTP 响应POST 仍是网络失败

类型化 API 发起逻辑 QUERY

当前 client 已记为不支持?

直接 POST path/_query

发送 QUERY path

响应结果

原样返回,不降级

记为不支持,再试一次 POST

试一次 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 协商规则。

当前 artifact 的精简结构
{
"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 标准 Operationpaths['/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 正文发送;现有模块方法以自己的契约写法为准,不要同时传两者。

SDK 模块方法
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 与认证请求头:

API Host CORS 配置
{
"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 原生 QUERY 结构
{
"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 OperationPOST /api/users/_queryQUERY /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 的同一版本里直接删除。先保留兼容入口并收集实际降级率;删除它应作为单独的破坏性变更,给出版本、遥测证据和消费方迁移期。

升级门禁

准备切换时逐项确认:

  1. .NET 11 已进入仓库允许的稳定发布通道,不再依赖 Preview SDK;
  2. OpenAPI artifact 的根版本为 3.2.0,主路径出现标准 query Operation;
  3. QUERY Operation 保留原 request body、响应、认证、Tag、摘要、错误模型和稳定 operationId;
  4. POST /_query 使用独立 operationId,并明确标为兼容入口,避免代码生成名称冲突;
  5. Scalar、OpenAPI lint、契约差异工具和 openapi-typescript 不会丢弃 query;
  6. generated.d.ts 的路径索引迁移已由契约别名吸收,页面没有直接引用 transport path;
  7. 浏览器分别通过原生 QUERY、强制 405、强制 501 和预检拒绝四条路径;
  8. CORS、CDN、WAF、Ingress 和 API Gateway 的方法白名单已在真实入口验证;
  9. 401、403、415、422 和 429 仍不会触发 POST 降级;
  10. 没有重新引入集合 GET,也没有在请求正文感知缓存完成前解除 BZEP005。

如果 .NET 11 已升级但下游工具还不能消费 3.2,应继续显式固定 OpenAPI 3.1。ASP.NET Core 11 在旧版本文档中可能把 QUERY 放到 x-oai-additionalOperations;这仍是扩展表示,不等于本项目已经完成 3.2 契约迁移。

9. 验证调用

下面两条命令应返回相同业务结果。ACCESS_TOKEN 来自正常登录流程,API 是待验证的真实入口:

Terminal window
# 通过真实入口验证首选 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}'

跨源部署还要直接验证预检:

Terminal window
# 预检应覆盖 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. 源码与规范

当前行为可从后端单仓根目录核对:

Terminal window
# 后端:确认生成路由、协议元数据和契约测试仍在。
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

协议依据与升级资料:

返回前端 · 平台 SDK · CORS 与响应头 · Web / API 构建块

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%