Skip to content
bitzorcas
中EN

Reference

HTTP QUERY and automatic POST fallback

How BitzOrcas uses RFC 10008 QUERY for paged, list, and complex read requests, including Platform SDK fallback, OpenAPI 3.1 representation, CORS, gateways, and a future .NET 11 / OpenAPI 3.2 migration.

Last updated

BitzOrcas models paging, lists, search, previews, and statistics as RFC 10008 QUERY requests. Query criteria travel as JSON content instead of being forced into the URL. GET is reserved for one resource representation identified by a stable route.

1. Method selection

Application pages normally call typed APIs, but the HTTP method still carries business meaning used by caching, audit, retry, and code-generation policy.

MethodUse it forDo not use it for
GETOne representation addressed by a stable route, with at most one simple scalar modifier such as language or formatCollections, paging, search, previews, statistics, complex objects, two or more non-route parameters, or request content
QUERYCollections, paging, search, complex filters, previews, statistics, and read-only validationCreation, update, deletion, notification, or ordinary Output Cache
POSTResource creation, non-idempotent actions, submitted processing, and the /_query compatibility transportThe preferred transport for an ordinary read query
PUTComplete replacement or idempotent upsert at a known resource URIPartial updates or non-idempotent actions
PATCHAn explicit, typed partial change to an existing resourceComplete replacement or untyped generic JSON Patch
DELETEDelete or revoke a resource addressed by a stable routeFiltered request content used to find resources

First-party JSON APIs do not retain a collection GET alias. A list with no filters still sends {}, keeping the method stable when filters are added later. Protocol endpoints such as OAuth/OIDC, SCIM, browser navigation, downloads, feeds, sitemaps, and health checks follow their own standards.

2. Wire contract

This is the current user-list request shape. QUERY is safe and idempotent; its Query Handler must not change business state.

Preferred entry point
QUERY /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9
Content-Type: application/json
Accept: application/json
{
"keyword": "Alex",
"status": "Active",
"pageIndex": 1,
"pageSize": 20,
"sortField": "displayName",
"sortOrder": "asc"
}

When the browser, gateway, or proxy chain cannot carry QUERY, the SDK sends the same content to the compatibility route:

Compatibility entry point
POST /api/users/_query HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9
Content-Type: application/json
Accept: application/json
{
"keyword": "Alex",
"status": "Active",
"pageIndex": 1,
"pageSize": 20,
"sortField": "displayName",
"sortOrder": "asc"
}

Both routes share the Query, Handler, authorization, tenant boundary, response DTO, status codes, and Problem Details. Only the HTTP method and path differ. Both responses advertise:

Accept-Query: application/json

The request contract also requires:

  • Content-Type: application/json;
  • non-empty content, using {} when there are no filters;
  • route tokens bound from the URL and overriding same-named content fields;
  • no GET compatibility alias;
  • no ordinary Output Cache, because a URL-only key cannot distinguish request content.

Moving criteria out of the URL does not make them confidential. Production still requires HTTPS and redaction of sensitive filters in gateway logs, request logging, and APM traces.

3. How Platform SDK falls back

createPlatformClient tracks QUERY support for the current transport chain in each client instance. Pages do not participate in this decision and must not catch an error to send their own POST retry.

yesnoordinary HTTP response405 or 501browser or proxy networkrejectionPOST receives an HTTPresponsePOST also has a networkfailure

Typed API starts a logical QUERY

Client already marked unsupported?

POST path/_query directly

Send QUERY path

Result

Return without fallback

Mark unsupported and try POST once

Try POST once

Mark unsupported

Keep unknown and return failure

Results that trigger fallback

ResultSDK behaviorRemembered as unsupported
405 Method Not AllowedRetry once with POST /_queryYes
501 Not ImplementedRetry once with POST /_queryYes
Fetch network errorRetry once with POST /_queryWhen POST receives any HTTP response

Browsers expose CORS preflight denial, DNS failure, and connection refusal as indistinguishable network errors. A network error therefore receives one POST attempt. QUERY must remain safe and idempotent because the first request may have reached the server before the second attempt begins.

Results that do not trigger fallback

Business and infrastructure responses such as 400, 401, 403, 404, 408, 409, 415, 422, 429, and any 5xx other than 501 do not trigger transport fallback.

  • 401 uses the normal access-token refresh flow and then repeats the same query transport;
  • 403 returns Forbidden; changing the method must never bypass authorization;
  • 415 means the caller did not send valid JSON content;
  • timeout and explicit cancellation do not send a POST retry;
  • validation or business failure is not repeated or hidden by fallback.

Capability state belongs only to the current PlatformClient. Reloading the page or recreating the Provider starts detection again. It is neither a server Feature nor persistent browser state.

4. Why OpenAPI 3.1 shows POST

The .NET 10 Host explicitly pins OpenAPI 3.1. The runtime QUERY route is hidden from the document. The POST fallback carries the standard request and response schemas plus five extensions:

See OpenAPI and Scalar documentation surface for Scalar enablement, Server selection, authentication, and reverse-proxy settings. Those runtime documentation settings do not change QUERY/POST negotiation.

Condensed current artifact shape
{
"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"
}
}
}
}
}
}
}
}

This creates an intentional difference between logical and generated surfaces:

ViewMethod and path
Application’s logical contractQUERY /api/users
Compatibility transportPOST /api/users/_query
OpenAPI 3.1 standard Operationpaths['/api/users/_query'].post
Current generated.d.ts type indexpaths['/api/users/_query']['post']

openapi-typescript generates content and response types from the POST Operation, but it does not negotiate runtime transport. Platform SDK module APIs continue to use the logical /api/users path and QUERY method. A third-party generator that ignores x-http-query-* should call the documented POST fallback explicitly, not invent a collection GET.

5. Frontend usage

Pages consume the typed API:

Page call
// Pages call the typed API and do not choose between QUERY and POST transports.
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) {
// Render the shared Problem Details model; do not send a page-level POST retry.
showProblem(result.error);
return;
}
renderUsers(result.data.items);

Only Platform SDK maintainers work with methods and paths. With method: 'QUERY', either body or query becomes JSON request content. Existing module methods keep their established form and never provide both.

SDK module method
searchUsers(query: UserQuery): Promise<PlatformResult<UserPage>> {
// Keep the logical resource path; the pipeline appends /_query when needed.
return client.request<UserPage>('/api/users', {
method: 'QUERY',
// Criteria are JSON content rather than URL query-string parameters.
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,
},
});
}

Do not write page-level fetch, concatenate /_query, or move content back to URLSearchParams. The shared pipeline owns auth refresh, tenant context, timeout, cancellation, Problem Details, and fallback.

6. CORS, proxies, and gateways

QUERY is not a CORS-safelisted method. Every cross-origin browser request starts with an OPTIONS preflight. Production must allow QUERY, the POST fallback, OPTIONS, JSON content, and authentication headers:

API Host CORS configuration
{
"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"
]
}
}

The API Host exposes Accept-Query through CORS. The edge must also be checked for:

  • CDN, WAF, ingress, OpenResty, and API Gateway support for an unfamiliar method;
  • method allowlists containing QUERY, POST, and OPTIONS;
  • routing rules that preserve the /_query suffix;
  • equivalent request-content size limits for QUERY and POST;
  • rate limits, audit, and metrics that aggregate both transports as one logical read;
  • Vite dev proxy forwarding QUERY without method rewriting.

The fallback still looks like POST to network infrastructure. Gateway policy should classify POST */_query as a read compatibility route. It must not count as resource creation or inherit write-command-only idempotency and audit classification merely because the transport method is POST. Origin and CSRF protections for cookie-based flows still follow the Host security policy.

7. Caching, retries, and observability

RFC 10008 permits caching a QUERY response only when the cache key accounts for request content and relevant request metadata. BitzOrcas’ ordinary Output Cache is URL-oriented, so generator diagnostic BZEP005 prevents its use on QUERY. The frontend must not assume that browsers or CDNs cache these requests.

Observability should retain logical and transport dimensions:

FieldExamplePurpose
Logical methodQUERYQuery volume, success, authorization, and latency
Transport methodQUERY or POSTLink compatibility
Logical path/api/usersAggregate one use case
Transport path/api/users or /api/users/_queryDiagnose routing and gateway behavior

A sudden increase in fallback rate points first to a proxy, CORS, or WAF change, not necessarily to a business endpoint regression.

8. What changes with .NET 11 and OpenAPI 3.2

OpenAPI 3.2 formally adds the Path Item query field. Starting with .NET 11 Preview 6, ASP.NET Core can emit MapMethods(..., ["QUERY"], ...) as a native QUERY Operation and defaults generated documents to OpenAPI 3.2.

That does not justify a preview runtime in production, and changing the Target Framework will not finish the migration by itself. API Host, Microsoft.AspNetCore.OpenApi, Microsoft.OpenApi, Scalar, validators, openapi-typescript, gateways, and consumer generators all need 3.2 compatibility evidence.

The native document shape is:

Native OpenAPI 3.2 QUERY shape
{
"openapi": "3.2.0",
"paths": {
"/api/users": {
"query": {
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BitzOrcasIdentityApplicationIdentitySearchUsersSearchUsersQuery"
}
}
}
}
}
}
}
}

Expected contract changes

ItemCurrent .NET 10 / OpenAPI 3.1Future .NET 11 / OpenAPI 3.2
Preferred runtime routeQUERY /api/usersUnchanged
POST compatibility routePOST /api/users/_queryRetained first; removal is separately versioned
Standard OpenAPI OperationPOST /api/users/_queryQUERY /api/users
QUERY descriptionx-http-query-*Standard query; extensions become transitional or are removed
Generated type indexpaths['/api/users/_query']['post']Expected to become paths['/api/users']['query']
Page callapi.searchUsers(...)Unchanged
Browser fallbackPlatform SDKStill Platform SDK

OpenAPI 3.2 standardizes description; it does not make every browser, proxy, or WAF accept QUERY. Do not remove POST fallback in the same release as the document switch. Keep it while measuring actual fallback usage. Removal is a separate breaking change with a version, telemetry evidence, and a consumer migration window.

Upgrade gate

Before switching:

  1. .NET 11 is in the repository’s permitted stable channel, not a Preview dependency;
  2. the artifact root is 3.2.0 and the primary path contains a standard query Operation;
  3. request body, responses, security, tags, summaries, error shapes, and stable operationId remain on QUERY;
  4. POST /_query has a separate operationId and is clearly marked as compatibility, avoiding generator collisions;
  5. Scalar, OpenAPI lint, contract diff, and openapi-typescript retain query;
  6. contract aliases absorb generated path-index changes so pages do not reference transport paths;
  7. browser tests cover native QUERY, forced 405, forced 501, and preflight rejection;
  8. CORS, CDN, WAF, ingress, and gateway allowlists pass at the real edge;
  9. 401, 403, 415, 422, and 429 still do not trigger fallback;
  10. collection GET does not return and BZEP005 stays until body-aware caching exists.

If the runtime has moved to .NET 11 but downstream tooling still requires 3.1, keep the version pinned explicitly. ASP.NET Core 11 can represent QUERY under x-oai-additionalOperations in older OpenAPI documents; that is still an extension and does not mean the BitzOrcas 3.2 migration is complete.

9. Verification calls

These requests should return the same business result. ACCESS_TOKEN comes from the normal sign-in flow and API is the real edge under test:

Terminal window
# Verify preferred QUERY through the real edge, not only a direct Host route.
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}'
# The fallback must receive exactly the same JSON content as QUERY.
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}'

For a cross-origin deployment, verify preflight directly:

Terminal window
# Exercise QUERY preflight, not only a conventional POST preflight.
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'

The response must allow the configured Origin, QUERY method, and headers. Postman and server-side test clients cannot prove browser CORS behavior.

10. Source and standards

From the backend monorepo root:

Terminal window
# Backend: retain generated routes, protocol metadata, and contract tests.
rg -n "HttpRoute.Query|HttpQueryProtocol|x-http-query|_query" \
src/Framework src/Platform src/Modules tests
# Frontend: keep logical QUERY and fallback inside Platform SDK.
rg -n "method: 'QUERY'|appendHttpQueryFallbackPath|shouldFallbackHttpQuery" \
frontend/packages/platform-sdk/src
# Artifact: stable output remains OpenAPI 3.1 with QUERY extensions today.
rg -n '"openapi": "3\.1|x-http-query-' artifacts/openapi/openapi-v1.json

Normative and upgrade references:

Back to Frontend · Platform SDK · CORS and headers · Web / API building block

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%