Use the HTTP QUERY method
By default Arc exposes every query over GET, with arguments in the URL query string. That is perfect until a query needs to carry a lot of arguments, or sensitive ones — a long free-text search, a nested filter, an access token. URLs have length limits, and everything in a URL leaks into server and proxy access logs, browser history, and Referer headers.
The HTTP QUERY method (RFC 10008) solves this: it is a safe, idempotent request — like GET — but it carries its arguments in a JSON request body instead of the URL.
Arc registers both verbs for every query endpoint, so QUERY is available whenever you want it. GET stays the default; you opt into QUERY on the client.
Use this when you want to:
- send query arguments that are too large for a URL
- keep sensitive arguments out of URLs, logs, and history
- move a growing filter object into a structured body
Prerequisites
Section titled “Prerequisites”- Your application is running and exposes at least one query.
- You are calling the query from the generated TypeScript proxy, or from a plain HTTP tool.
Declare the transport in C#
Section titled “Declare the transport in C#”The most direct way is to declare the transport where the query lives — the same place you already put [Route] or [AllowAnonymous]. Put [QueryHttpMethod] on a read model (all its queries) or a single static query method, and the generated proxy defaults to that transport with no client wiring:
[ReadModel]public record Order(OrderId Id, string Customer){ [QueryHttpMethod(QueryHttpMethod.Query)] public static IEnumerable<Order> Search(OrderFilter filter) => /* ... */;}The generated Search proxy calls setHttpMethod(QueryHttpMethod.Query) in its constructor, so every caller uses QUERY automatically — the backend author’s knowledge that this query takes a large filter flows to the client through proxy generation. A method-level attribute overrides a read-model-level one, callers can still override it at runtime with setHttpMethod, and the server accepts both verbs regardless — this only sets the client default.
Opt in from the client
Section titled “Opt in from the client”The generated proxies default to GET. Switch the default for every query by setting Globals.queryHttpMethod:
import { Globals } from '@cratis/arc';import { QueryHttpMethod } from '@cratis/arc/queries';
Globals.queryHttpMethod = QueryHttpMethod.Query;To switch a single query without changing the global default, call setHttpMethod on the query instance:
query.setHttpMethod(QueryHttpMethod.Query);Everything else — arguments, paging, sorting, the shape of the result — stays exactly the same. Only the transport changes.
Let the framework choose with Auto
Section titled “Let the framework choose with Auto”If you’re not sure every deployment’s network path supports QUERY (some corporate proxies, WAFs and gateways don’t recognize it yet), use QueryHttpMethod.Auto:
import { Globals } from '@cratis/arc';import { QueryHttpMethod } from '@cratis/arc/queries';
Globals.queryHttpMethod = QueryHttpMethod.Auto;Arc sends QUERY on the first query. If the server or an intermediary rejects the verb — a 405/501 response, or a network/CORS error from fetch — it transparently retries the query with GET and remembers the outcome per backend (origin + API base path) for the rest of the session, so subsequent queries go straight to the working transport and one backend’s lack of support never downgrades another. This also covers the cross-origin case: if the CORS policy doesn’t allow QUERY, Auto simply settles on GET.
Auto only falls back on transport-level failures — an application error (a normal failed QueryResult) is never retried as GET. Call resetQueryHttpMethodResolution() (from @cratis/arc/queries) to make the next Auto query probe again, for example after a network change.
Choose the transport per query
Section titled “Choose the transport per query”Most queries have small arguments that belong in the URL — only the ones whose arguments overflow it really need QUERY. Instead of picking a method for the whole app, set a resolver that decides per query. The built-in lengthBasedQueryHttpMethod keeps short queries on cacheable GET and prefers QUERY only when the GET URL would exceed a threshold:
import { Globals } from '@cratis/arc';import { lengthBasedQueryHttpMethod } from '@cratis/arc/queries';
Globals.queryHttpMethodResolver = lengthBasedQueryHttpMethod({ threshold: 2000 });When the URL is short the query uses GET; when it exceeds the threshold it uses QUERY (with Auto’s GET fallback, so an unsupporting backend still degrades gracefully).
The resolver is consulted only when a query has no explicit method set via setHttpMethod — an explicit per-query choice always wins. You can also write your own policy; it receives the built GET URL, the route and the arguments:
import { QueryHttpMethod } from '@cratis/arc/queries';
Globals.queryHttpMethodResolver = ({ route }) => route.startsWith('/api/reports') ? QueryHttpMethod.Query : QueryHttpMethod.Get;The request body
Section titled “The request body”With QUERY, route parameters stay in the path (they identify the resource); every other argument, plus paging and sorting, moves into a JSON body:
{ "arguments": { "searchText": "a very long search expression", "status": "active" }, "paging": { "page": 0, "pageSize": 20 }, "sorting": { "field": "name", "direction": "asc" }}paging and sorting are optional — omit them for an unpaged, unsorted query.
Call it with cURL
Section titled “Call it with cURL”You can exercise a QUERY endpoint with any HTTP client. The Content-Type must be application/json — Arc rejects a request without it.
curl -X QUERY "https://localhost:5001/api/orders/search" \ -H "Content-Type: application/json" \ -d '{ "arguments": { "searchText": "widgets" }, "paging": { "page": 0, "pageSize": 20 } }'The response is the same QueryResult JSON you get from the GET form of the query.
Cross-origin calls need CORS to allow QUERY
Section titled “Cross-origin calls need CORS to allow QUERY”QUERY is not a simple method, so a browser sends a preflight OPTIONS request first. If you call queries from another origin, add QUERY to your allowed methods:
builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.WithMethods("GET", "POST", "QUERY").AllowAnyHeader().AllowAnyOrigin()));The GET default needs no CORS change, so this only matters once you opt into QUERY.
Turn it off on the server
Section titled “Turn it off on the server”QUERY endpoints are registered by default. To restrict query endpoints to GET only — for example behind infrastructure that rejects unknown HTTP verbs — disable it:
builder.Services.Configure<ArcOptions>(options => options.GeneratedApis.EnableQueryHttpMethod = false);GET is unaffected.