Eine REST-API auf Basis von TsgcHTTPServer zu bauen war schon immer möglich, aber ein paar Dinge mussten jedes einzelne Mal von Hand geschrieben werden: die CORS-Preflight-Antwort, ein Health-Endpunkt für den Load Balancer, ein Metrik-Endpunkt für den Monitoring-Stack und irgendeine Möglichkeit, die Daten des einen Kunden von denen des anderen zu unterscheiden. TsgcHTTPRESTServer ist eine neue Komponente, die all das bereits mitbringt.
Sie leitet sich direkt von TsgcHTTPServer ab, alles, was Sie bereits kennen, gilt also weiterhin: derselbe Port, dieselben SSLOptions, dieselbe Authentication, derselbe OnCommandGet-Handler. TsgcHTTPServer bleibt ein einfacher HTTP-Server und ändert sich nicht. Die Erweiterungen stecken im Nachfahren, und jede einzelne davon muss ausdrücklich aktiviert werden.
Erste Schritte
Die Komponente befindet sich in der Unit sgcHTTP_REST_Server und ist auf der Palettenseite SGC REST als TsgcHTTPRESTServer registriert. Sie auf ein Formular zu ziehen und Active zu setzen ergibt einen funktionierenden HTTP-Server; interessant wird es beim Request-Handler.
uses
sgcHTTP_REST_Server;
var
oServer: TsgcHTTPRESTServer;
begin
oServer := TsgcHTTPRESTServer.Create(nil);
oServer.Port := 8080;
oServer.OnCommandGet := OnServerCommandGet;
oServer.Active := True;
end;
Der Handler verwendet die Standard-Request- und -Response-Objekte von Indy, eine JSON-Antwort besteht also aus drei Zuweisungen:
procedure TForm1.OnServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
if ARequestInfo.Document = '/api/status' then
begin
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := '{"status":"running"}';
end
else
begin
AResponseInfo.ResponseNo := 404;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := '{"error":"not found"}';
end;
end;
OnCommandGet empfängt GET und POST. Verben wie PUT, PATCH und DELETE kommen in OnCommandOther an, das exakt dieselbe Signatur hat. Eine REST-Ressource, die den vollständigen Satz an Verben unterstützt, wird deshalb üblicherweise als eine einzige Dispatch-Routine geschrieben, die aus beiden Ereignissen aufgerufen wird.
procedure TForm1.OnServerCommandOther(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
if ARequestInfo.Command = 'DELETE' then
begin
AResponseInfo.ResponseNo := 204;
AResponseInfo.ContentText := '';
end;
end;
CORS, mit Absicht standardmäßig deaktiviert
Ein Browser, der Ihre API von einem anderen Origin aus aufruft, braucht die Access-Control-*-Header, und er braucht eine Antwort auf den OPTIONS-Preflight, bevor er den eigentlichen Request sendet. CORSOptions erledigt beides.
Hervorzuheben ist vor allem, dass Enabled standardmäßig False ist und das auch bleiben soll, solange Sie es nicht wirklich brauchen. Ein Server, der nach einem Update stillschweigend mit Access-Control-Allow-Origin: * zu antworten beginnt, wäre eine Sicherheitsregression, deshalb muss CORS strikt ausdrücklich aktiviert werden.
oServer.CORSOptions.Enabled := True;
oServer.CORSOptions.AllowOrigins := 'https://app.example.com';
oServer.CORSOptions.AllowMethods := 'GET, POST, PUT, DELETE, OPTIONS';
oServer.CORSOptions.AllowHeaders := 'Content-Type, Authorization';
Damit wird der Preflight automatisch mit einem 204 und den drei Headern beantwortet, und jede normale Antwort erhält die Header hinzugefügt. Sie schreiben keinen OPTIONS-Zweig in Ihrem Handler.
Ziehen Sie einen expliziten Origin dem * vor, sobald die API authentifiziert ist. Ein Wildcard-Origin zusammen mit Credentials ist eine Kombination, die Browser ohnehin ablehnen, und die Origins aufzuzählen, die Sie tatsächlich bedienen, ist die sicherere Voreinstellung.
Health und Metriken, ohne sie zu schreiben
Hängen Sie eine TsgcHTTPServerStats-Komponente an die Eigenschaft ServerStats, und der Server kann zwei betriebliche Endpunkte beantworten. Beide sind deaktiviert, bis Sie sie einzeln einschalten.
oStats := TsgcHTTPServerStats.Create(nil);
oStats.Endpoints.Health.Enabled := True;
oStats.Endpoints.Metrics.Enabled := True;
oServer.ServerStats := oStats;
/health antwortet mit einem kleinen JSON-Dokument, das sich für eine Load-Balancer-Prüfung eignet:
{"status":"ok","uptime":3600,"connections":12,"requests":48120,
"responses":{"1xx":0,"2xx":47800,"3xx":10,"4xx":300,"5xx":10},
"latency":{"min":0,"avg":4,"max":180}}
/metrics antwortet im Prometheus Text Exposition Format, es lässt sich also ohne zwischengeschalteten Adapter scrapen:
# HELP sgc_server_requests_total Total requests served
# TYPE sgc_server_requests_total counter
sgc_server_requests_total 48120
# HELP sgc_server_request_duration_ms_avg Average request duration
# TYPE sgc_server_request_duration_ms_avg gauge
sgc_server_request_duration_ms_avg 4
Beide Pfade sind konfigurierbar, falls /metrics und /health mit Ihren eigenen Routen kollidieren:
oStats.Endpoints.Metrics.Path := '/internal/metrics';
oStats.Endpoints.Health.Path := '/internal/health';
Diese Endpunkte werden nach der Authentifizierungsprüfung ausgeliefert, sie erben also die Authentifizierung, die der Server ohnehin erzwingt. Sie sind nie öffentlich, es sei denn, der Server selbst ist öffentlich. Das ist in beide Richtungen wissenswert: es hält sie standardmäßig privat, und es bedeutet, dass ein Monitoring-Scraper Credentials braucht, wenn der Server welche verlangt.
Statische Inhalte neben der API ausliefern
DocumentRoot wird von TsgcHTTPServer geerbt und funktioniert weiterhin, ein einzelner Server kann also ein kleines Frontend und die API hosten, mit der es spricht. Alles, was Ihr Handler nicht beantwortet, fällt auf das Document Root durch.
oServer.DocumentRoot := 'C:\www\app';
oServer.HTTPCompression.Enabled := True;
oServer.HTTPCompression.MinSize := 1024;
TLS
Gegenüber dem Basis-Server ändert sich nichts. Setzen Sie SSL und füllen Sie SSLOptions wie gewohnt:
oServer.SSL := True;
oServer.SSLOptions.Port := 443;
oServer.Port := 443;
Wie es weitergeht
Die Komponente veröffentlicht außerdem eine Tenancy-Eigenschaft und eine schreibgeschützte Tenant-Eigenschaft, die aus einem einzelnen Server einen mandantenfähigen machen, und die Authentication-Optionen akzeptieren eine User-Store-Komponente, sodass Sie Credentials nicht in einer TStringList aufbewahren müssen. Darum geht es im zweiten Artikel, und der dritte zeigt, wie Sie dem Ganzen einen OpenAPI-Vertrag voranstellen, sodass Routen, Validierung und Dokumentation alle aus einer einzigen Datei stammen.
TsgcHTTPRESTServer ist ab sofort verfügbar. Laden Sie den aktuellen Build von der sgcWebSockets Download-Seite herunter.
