The IETF HTTP Working Group has specified a new HTTP request method called QUERY (draft-ietf-httpbis-safe-method-w-body, on its way to becoming an RFC). QUERY fills a gap that every REST developer has run into: it is a safe and idempotent request, like GET, but it carries a request body, like POST. sgcWebSockets now supports QUERY end to end, in the HTTP/1.1, HTTP/2 and HTTP/3 clients and in the server components, for Delphi and C++ Builder.
Why a new HTTP method?
Until now there were two ways to send a query to a server, and both have problems. You can encode the query into the URL of a GET request, but URLs have practical length limits, complex queries are painful to escape, and the full URL tends to end up in proxy and server logs, which is a bad place for sensitive filter values. Or you can send the query in the body of a POST request, but POST is not safe, not idempotent and not cacheable: an HTTP client is not allowed to retry it automatically after a connection failure, and caches will not store the response.
QUERY combines the best of both. The request content and its Content-Type define the query, so it can be as large and as structured as needed (form data, JSON, SQL, JSONPath, whatever the server accepts). At the same time the specification declares QUERY safe and idempotent: it never changes the state of the resource, it can be retried or repeated safely, and its responses are cacheable just like a GET response.
How QUERY works
A QUERY request looks like a POST with GET semantics. This example, adapted from the specification, asks a contacts resource for a filtered projection:
QUERY /contacts HTTP/1.1
Host: example.org
Content-Type: application/x-www-form-urlencoded
Accept: application/json
select=surname,givenname,email&limit=10
HTTP/1.1 200 OK
Content-Type: application/json
[
{ "surname": "Smith", "givenname": "John", "email": "smith@example.org" },
{ "surname": "Jones", "givenname": "Sally", "email": "sally.jones@example.com" }
]
The most important rules from the specification:
- Every QUERY request must include a
Content-Typethat matches its body. A server must reject the request with a 4xx status (400, 415 or 422) when the header is missing or inconsistent with the content. - Responses are cacheable. A cache that stores them must include the request body in the cache key, so two different queries never collide.
- Redirects preserve the method: after a 301, 302, 307 or 308 the client repeats the QUERY against the new URI. The old exception that rewrites a redirected POST into a GET does not apply to QUERY. A 303 See Other is the explicit way for a server to say "fetch the result with a GET from this Location".
- A server can advertise which query formats a resource understands with the new
Accept-Queryresponse header, and QUERY appears in theAllowheader of an OPTIONS response when supported. - In browsers, QUERY is not a CORS-safelisted method, so cross-origin calls trigger a preflight. The CORS defaults of the sgcWebSockets OpenAPI server already include QUERY in
Access-Control-Allow-Methods.
QUERY from the HTTP/1.1 client
The HTTP/1.1 client gains Query and QueryAsync methods that work exactly like their Post counterparts: pass the URL and a stream with the query body, set the Content-Type in the request, and read the result.
uses
Classes, sgcHTTP;
var
oClient: TsgcHTTP1Client;
oQuery: TStringStream;
vResult: string;
begin
oClient := TsgcHTTP1Client.Create(nil);
try
oQuery := TStringStream.Create('select=surname,givenname,email&limit=10');
try
oClient.Request.ContentType := 'application/x-www-form-urlencoded';
oClient.Request.Accept := 'application/json';
vResult := oClient.Query('https://api.example.org/contacts', oQuery);
finally
oQuery.Free;
end;
finally
oClient.Free;
end;
end;
Because QUERY is idempotent, it also plays well with the client retry options: a QUERY that fails with a connection error can be retried automatically without the risk of duplicating a state change on the server. The same Query methods are available on the REST API base client, so custom API integrations built on sgcWebSockets can adopt the verb with no extra work.
QUERY over HTTP/2 and HTTP/3
The HTTP/2 and HTTP/3 clients expose the same verb. Over HTTP/2 the request is sent with the :method pseudo-header set to QUERY and the body in DATA frames:
uses
Classes, sgcHTTP;
var
oClient: TsgcHTTP2Client;
oQuery: TStringStream;
vResult: string;
begin
oClient := TsgcHTTP2Client.Create(nil);
try
oQuery := TStringStream.Create('select=surname,givenname,email&limit=10');
try
vResult := oClient.Query('https://api.example.org/contacts', oQuery);
finally
oQuery.Free;
end;
finally
oClient.Free;
end;
end;
The HTTP/3 client offers a string overload with the form-urlencoded content type as default, which keeps simple queries to a single call:
uses
sgcHTTP;
var
oClient: TsgcHTTP3Client;
vResult: string;
begin
oClient := TsgcHTTP3Client.Create(nil);
try
vResult := oClient.Query('https://api.example.org/contacts',
'select=surname,givenname,email&limit=10');
finally
oClient.Free;
end;
end;
Handling QUERY on the server
On the server side, TsgcWebSocketHTTPServer accepts QUERY requests over HTTP/1.1 and HTTP/2 and dispatches them to the OnCommandOther event, with the request body already read and available in PostStream. A minimal handler validates the Content-Type, runs the query and returns the result:
procedure TForm1.ServerCommandOther(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
vQuery: string;
begin
if SameText(ARequestInfo.Command, 'QUERY') then
begin
// the specification requires a Content-Type on every QUERY request
if (ARequestInfo.ContentType = '') or
not Assigned(ARequestInfo.PostStream) then
begin
AResponseInfo.ResponseNo := 400;
Exit;
end;
ARequestInfo.PostStream.Position := 0;
vQuery := ReadStringFromStream(ARequestInfo.PostStream);
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := RunContactsQuery(vQuery);
end;
end;
The HTTP/3 server reads QUERY bodies the same way, the OpenAPI server routes query operations declared in an OpenAPI spec and advertises the method in its CORS defaults, and the HTTP forwarding feature proxies QUERY requests with their body to a backend server.
Availability
QUERY support is included in sgcWebSockets 2026.7 across the HTTP/1.1, HTTP/2 and HTTP/3 clients, the REST API base client, the server components and the OpenAPI server, for Delphi 7 to Delphi 13 and C++ Builder. Since the underlying wire format is plain HTTP, it interoperates today with any server or client that understands the method. You can download the latest version from the sgcWebSockets download page.
Questions, feedback or migration help? Get in touch, you will get a reply from the people who wrote the code.
