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.
| Method | Use it for | Do not use it for |
|---|---|---|
GET | One representation addressed by a stable route, with at most one simple scalar modifier such as language or format | Collections, paging, search, previews, statistics, complex objects, two or more non-route parameters, or request content |
QUERY | Collections, paging, search, complex filters, previews, statistics, and read-only validation | Creation, update, deletion, notification, or ordinary Output Cache |
POST | Resource creation, non-idempotent actions, submitted processing, and the /_query compatibility transport | The preferred transport for an ordinary read query |
PUT | Complete replacement or idempotent upsert at a known resource URI | Partial updates or non-idempotent actions |
PATCH | An explicit, typed partial change to an existing resource | Complete replacement or untyped generic JSON Patch |
DELETE | Delete or revoke a resource addressed by a stable route | Filtered 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.
QUERY /api/users HTTP/1.1Host: api.example.comAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/jsonAccept: 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:
POST /api/users/_query HTTP/1.1Host: api.example.comAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9Content-Type: application/jsonAccept: 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/jsonThe 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
GETcompatibility 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.
Results that trigger fallback
| Result | SDK behavior | Remembered as unsupported |
|---|---|---|
405 Method Not Allowed | Retry once with POST /_query | Yes |
501 Not Implemented | Retry once with POST /_query | Yes |
| Fetch network error | Retry once with POST /_query | When 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.
401uses the normal access-token refresh flow and then repeats the same query transport;403returnsForbidden; changing the method must never bypass authorization;415means 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.
{ "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:
| View | Method and path |
|---|---|
| Application’s logical contract | QUERY /api/users |
| Compatibility transport | POST /api/users/_query |
| OpenAPI 3.1 standard Operation | paths['/api/users/_query'].post |
Current generated.d.ts type index | paths['/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:
// 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.
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:
{ "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, andOPTIONS; - routing rules that preserve the
/_querysuffix; - 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:
| Field | Example | Purpose |
|---|---|---|
| Logical method | QUERY | Query volume, success, authorization, and latency |
| Transport method | QUERY or POST | Link compatibility |
| Logical path | /api/users | Aggregate one use case |
| Transport path | /api/users or /api/users/_query | Diagnose 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:
{ "openapi": "3.2.0", "paths": { "/api/users": { "query": { "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BitzOrcasIdentityApplicationIdentitySearchUsersSearchUsersQuery" } } } } } } }}Expected contract changes
| Item | Current .NET 10 / OpenAPI 3.1 | Future .NET 11 / OpenAPI 3.2 |
|---|---|---|
| Preferred runtime route | QUERY /api/users | Unchanged |
| POST compatibility route | POST /api/users/_query | Retained first; removal is separately versioned |
| Standard OpenAPI Operation | POST /api/users/_query | QUERY /api/users |
| QUERY description | x-http-query-* | Standard query; extensions become transitional or are removed |
| Generated type index | paths['/api/users/_query']['post'] | Expected to become paths['/api/users']['query'] |
| Page call | api.searchUsers(...) | Unchanged |
| Browser fallback | Platform SDK | Still 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:
- .NET 11 is in the repository’s permitted stable channel, not a Preview dependency;
- the artifact root is
3.2.0and the primary path contains a standardqueryOperation; - request body, responses, security, tags, summaries, error shapes, and stable operationId remain on QUERY;
- POST
/_queryhas a separate operationId and is clearly marked as compatibility, avoiding generator collisions; - Scalar, OpenAPI lint, contract diff, and
openapi-typescriptretainquery; - contract aliases absorb generated path-index changes so pages do not reference transport paths;
- browser tests cover native QUERY, forced 405, forced 501, and preflight rejection;
- CORS, CDN, WAF, ingress, and gateway allowlists pass at the real edge;
401,403,415,422, and429still do not trigger fallback;- collection GET does not return and
BZEP005stays 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:
# 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:
# 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:
# 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.jsonNormative and upgrade references:
- RFC 10008: The HTTP QUERY Method
- OpenAPI 3.2 Path Item Object
- HTTP QUERY and OpenAPI 3.2 in ASP.NET Core 11
- ASP.NET Core 11 OpenAPI 3.2 default-version change
Back to Frontend · Platform SDK · CORS and headers · Web / API building block