The New HTTP QUERY Method in Delphi | eSeGeCe Blog

The New HTTP QUERY Method in Delphi

· Features
The New HTTP QUERY Method in Delphi

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:

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.