Die IETF HTTP Working Group hat eine neue HTTP-Anfragemethode namens QUERY spezifiziert (draft-ietf-httpbis-safe-method-w-body, auf dem Weg zum RFC). QUERY schließt eine Lücke, auf die jeder REST-Entwickler schon gestoßen ist: Es ist eine sichere und idempotente Anfrage wie GET, trägt aber einen Request-Body wie POST. sgcWebSockets unterstützt QUERY jetzt durchgängig, in den HTTP/1.1-, HTTP/2- und HTTP/3-Clients sowie in den Serverkomponenten, für Delphi und C++ Builder.
Warum eine neue HTTP-Methode?
Bislang gab es zwei Möglichkeiten, eine Abfrage an einen Server zu senden, und beide haben Probleme. Man kann die Abfrage in die URL einer GET-Anfrage kodieren, aber URLs haben praktische Längenbeschränkungen, komplexe Abfragen sind mühsam zu maskieren, und die vollständige URL landet meist in Proxy- und Serverprotokollen, ein schlechter Ort für sensible Filterwerte. Oder man sendet die Abfrage im Body einer POST-Anfrage, aber POST ist weder sicher noch idempotent noch cachefähig: Ein HTTP-Client darf die Anfrage nach einem Verbindungsfehler nicht automatisch wiederholen, und Caches speichern die Antwort nicht.
QUERY kombiniert das Beste aus beiden Welten. Der Inhalt der Anfrage und ihr Content-Type definieren die Abfrage, sie kann also so groß und so strukturiert sein wie nötig (Formulardaten, JSON, SQL, JSONPath, was auch immer der Server akzeptiert). Gleichzeitig erklärt die Spezifikation QUERY für sicher und idempotent: Die Anfrage ändert nie den Zustand der Ressource, sie kann gefahrlos wiederholt werden, und ihre Antworten sind genau wie eine GET-Antwort cachefähig.
So funktioniert QUERY
Eine QUERY-Anfrage sieht aus wie ein POST mit GET-Semantik. Dieses Beispiel, angelehnt an die Spezifikation, fragt eine Kontakte-Ressource nach einer gefilterten Projektion:
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" }
]
Die wichtigsten Regeln der Spezifikation:
- Jede QUERY-Anfrage muss einen
Content-Typeenthalten, der zu ihrem Body passt. Ein Server muss die Anfrage mit einem 4xx-Status (400, 415 oder 422) ablehnen, wenn der Header fehlt oder nicht zum Inhalt passt. - Antworten sind cachefähig. Ein Cache, der sie speichert, muss den Request-Body in den Cache-Schlüssel aufnehmen, damit zwei unterschiedliche Abfragen nie kollidieren.
- Redirects behalten die Methode bei: Nach einem 301, 302, 307 oder 308 wiederholt der Client das QUERY gegen die neue URI. Die alte Ausnahme, die ein umgeleitetes POST in ein GET umschreibt, gilt für QUERY nicht. Ein 303 See Other ist der explizite Weg für einen Server zu sagen: "Hole das Ergebnis mit einem GET von dieser Location ab".
- Ein Server kann mit dem neuen Response-Header
Accept-Querybekanntgeben, welche Abfrageformate eine Ressource versteht, und QUERY erscheint imAllow-Header einer OPTIONS-Antwort, wenn die Methode unterstützt wird. - In Browsern ist QUERY keine CORS-safelisted Methode, Cross-Origin-Aufrufe lösen daher einen Preflight aus. Die CORS-Voreinstellungen des sgcWebSockets-OpenAPI-Servers enthalten QUERY bereits in
Access-Control-Allow-Methods.
QUERY mit dem HTTP/1.1-Client
Der HTTP/1.1-Client erhält die Methoden Query und QueryAsync, die genau wie ihre Post-Gegenstücke funktionieren: URL und einen Stream mit dem Abfrage-Body übergeben, den Content-Type in der Anfrage setzen und das Ergebnis lesen.
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;
Da QUERY idempotent ist, harmoniert es auch gut mit den Retry-Optionen des Clients: Ein QUERY, das mit einem Verbindungsfehler fehlschlägt, kann automatisch wiederholt werden, ohne dass die Gefahr besteht, eine Zustandsänderung auf dem Server zu duplizieren. Dieselben Query-Methoden stehen im REST-API-Basisclient zur Verfügung, sodass eigene API-Integrationen auf Basis von sgcWebSockets das Verb ohne zusätzlichen Aufwand übernehmen können.
QUERY über HTTP/2 und HTTP/3
Die HTTP/2- und HTTP/3-Clients bieten dasselbe Verb. Über HTTP/2 wird die Anfrage mit dem auf QUERY gesetzten :method-Pseudo-Header gesendet, der Body geht 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;
Der HTTP/3-Client bietet eine String-Überladung mit dem Form-urlencoded-Content-Type als Standard, damit bleiben einfache Abfragen ein einziger Aufruf:
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;
QUERY auf dem Server verarbeiten
Auf der Serverseite akzeptiert TsgcWebSocketHTTPServer QUERY-Anfragen über HTTP/1.1 und HTTP/2 und leitet sie an das Ereignis OnCommandOther weiter, wobei der Request-Body bereits gelesen wurde und in PostStream verfügbar ist. Ein minimaler Handler validiert den Content-Type, führt die Abfrage aus und liefert das Ergebnis zurück:
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;
Der HTTP/3-Server liest QUERY-Bodys auf dieselbe Weise, der OpenAPI-Server routet in einer OpenAPI-Spezifikation deklarierte query-Operationen und kündigt die Methode in seinen CORS-Voreinstellungen an, und die HTTP-Weiterleitungsfunktion reicht QUERY-Anfragen samt Body als Proxy an einen Backend-Server durch.
Verfügbarkeit
Die QUERY-Unterstützung ist in sgcWebSockets 2026.7 enthalten, in den HTTP/1.1-, HTTP/2- und HTTP/3-Clients, im REST-API-Basisclient, in den Serverkomponenten und im OpenAPI-Server, für Delphi 7 bis Delphi 13 und C++ Builder. Da das zugrunde liegende Wire-Format reines HTTP ist, interoperiert es schon heute mit jedem Server oder Client, der die Methode versteht. Die aktuelle Version können Sie von der sgcWebSockets-Downloadseite herunterladen.
Fragen, Feedback oder Hilfe bei der Migration? Kontaktieren Sie uns, Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.
