TsgcHTTPRESTServer : le nouveau composant serveur REST | Blog eSeGeCe

TsgcHTTPRESTServer : le nouveau composant serveur REST

· Composants
Composant sgcWebSockets TsgcHTTPRESTServer

Construire une API REST par-dessus TsgcHTTPServer a toujours été possible, mais quelques éléments devaient être écrits à la main à chaque fois : la réponse au préflight CORS, un endpoint de santé pour le répartiteur de charge, un endpoint de métriques pour la pile de supervision, et un moyen de distinguer les données d'un client de celles d'un autre. TsgcHTTPRESTServer est un nouveau composant qui livre tout cela d'origine.

Il descend directement de TsgcHTTPServer, donc tout ce que vous connaissez déjà reste valable : le même Port, les mêmes SSLOptions, la même Authentication, le même gestionnaire OnCommandGet. TsgcHTTPServer reste un simple serveur HTTP et ne change pas. Les extras vivent dans le descendant, et chacun d'eux est optionnel.

Premiers pas

Le composant se trouve dans l'unité sgcHTTP_REST_Server et est enregistré sur la page de palette SGC REST sous le nom TsgcHTTPRESTServer. Le déposer sur une fiche et définir Active vous donne un serveur HTTP fonctionnel ; la partie intéressante est le gestionnaire de requêtes.

uses
  sgcHTTP_REST_Server;

var
  oServer: TsgcHTTPRESTServer;
begin
  oServer := TsgcHTTPRESTServer.Create(nil);
  oServer.Port := 8080;
  oServer.OnCommandGet := OnServerCommandGet;
  oServer.Active := True;
end;

Le gestionnaire utilise les objets requête et réponse standard d'Indy, donc une réponse JSON tient en trois affectations :

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 reçoit les GET et les POST. Les verbes tels que PUT, PATCH et DELETE arrivent dans OnCommandOther, qui possède exactement la même signature, donc une ressource REST qui prend en charge l'ensemble des verbes s'écrit généralement comme une seule routine de dispatch appelée depuis les deux événements.

procedure TForm1.OnServerCommandOther(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  if ARequestInfo.Command = 'DELETE' then
  begin
    AResponseInfo.ResponseNo := 204;
    AResponseInfo.ContentText := '';
  end;
end;

CORS, désactivé par défaut à dessein

Un navigateur qui appelle votre API depuis une autre origine a besoin des en-têtes Access-Control-*, et il lui faut une réponse au préflight OPTIONS avant d'envoyer la vraie requête. CORSOptions gère les deux.

Le point qui mérite d'être souligné est que Enabled vaut False par défaut et qu'il est censé le rester tant que vous n'en avez pas réellement besoin. Un serveur qui se mettrait discrètement à répondre avec Access-Control-Allow-Origin: * après une mise à jour constituerait une régression de sécurité, donc CORS est strictement optionnel.

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';

Avec cela en place, le préflight reçoit automatiquement une réponse 204 accompagnée des trois en-têtes, et chaque réponse normale se voit ajouter les en-têtes. Vous n'écrivez aucune branche OPTIONS dans votre gestionnaire.

Préférez une origine explicite à * dès que l'API est authentifiée. Une origine joker combinée à des identifiants est de toute façon rejetée par les navigateurs, et énumérer les origines que vous servez réellement est le choix par défaut le plus sûr.

Santé et métriques sans les écrire

Attachez un composant TsgcHTTPServerStats à la propriété ServerStats et le serveur peut répondre à deux endpoints opérationnels. Les deux sont désactivés tant que vous ne les activez pas individuellement.

oStats := TsgcHTTPServerStats.Create(nil);
oStats.Endpoints.Health.Enabled := True;
oStats.Endpoints.Metrics.Enabled := True;
oServer.ServerStats := oStats;

/health répond par un petit document JSON adapté à une sonde de répartiteur de charge :

{"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 répond au format d'exposition texte de Prometheus, il peut donc être collecté sans adaptateur intermédiaire :

# 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

Les deux chemins sont configurables si /metrics et /health entrent en conflit avec vos propres routes :

oStats.Endpoints.Metrics.Path := '/internal/metrics';
oStats.Endpoints.Health.Path := '/internal/health';

Ces endpoints sont servis après la barrière d'authentification, ils héritent donc de l'authentification que le serveur applique déjà. Ils ne sont jamais publics à moins que le serveur lui-même ne le soit. Cela mérite d'être connu dans les deux sens : cela les garde privés par défaut, et cela signifie qu'un collecteur de supervision a besoin d'identifiants lorsque le serveur en exige.

Servir du contenu statique à côté de l'API

DocumentRoot est hérité de TsgcHTTPServer et fonctionne toujours, donc un seul serveur peut héberger une petite interface et l'API avec laquelle elle dialogue. Tout ce à quoi votre gestionnaire ne répond pas retombe sur la racine des documents.

oServer.DocumentRoot := 'C:\www\app';
oServer.HTTPCompression.Enabled := True;
oServer.HTTPCompression.MinSize := 1024;

TLS

Rien ne change par rapport au serveur de base. Définissez SSL et remplissez SSLOptions comme d'habitude :

oServer.SSL := True;
oServer.SSLOptions.Port := 443;
oServer.Port := 443;

La suite

Le composant publie également une propriété Tenancy et une propriété en lecture seule Tenant, qui transforment un serveur unique en serveur multi-clients, et les options d'Authentication acceptent un composant de stockage d'utilisateurs pour que vous n'ayez plus à conserver les identifiants dans une TStringList. Ces points sont traités dans le deuxième article, et le troisième montre comment placer un contrat OpenAPI devant l'ensemble pour que les routes, la validation et la documentation proviennent toutes d'un seul fichier.

TsgcHTTPRESTServer est disponible dès maintenant. Téléchargez la dernière version depuis la page de téléchargement de sgcWebSockets.