Construir una API REST sobre TsgcHTTPServer siempre ha sido posible, pero había unas cuantas cosas que se tenían que escribir a mano una y otra vez: la respuesta al preflight CORS, un endpoint de salud para el balanceador de carga, un endpoint de métricas para el sistema de monitorización y alguna forma de distinguir los datos de un cliente de los de otro. TsgcHTTPRESTServer es un componente nuevo que trae todo eso de serie.
Desciende directamente de TsgcHTTPServer, así que todo lo que ya conoce sigue siendo válido: el mismo Port, las mismas SSLOptions, la misma Authentication, el mismo manejador OnCommandGet. TsgcHTTPServer sigue siendo un servidor HTTP normal y no cambia. Los extras viven en el descendiente y cada uno de ellos hay que activarlo de forma explícita.
Primeros pasos
El componente reside en la unidad sgcHTTP_REST_Server y se registra en la página de paleta SGC REST como TsgcHTTPRESTServer. Colocarlo en un formulario y activar Active ya le da un servidor HTTP en funcionamiento; la parte interesante es el manejador de peticiones.
uses
sgcHTTP_REST_Server;
var
oServer: TsgcHTTPRESTServer;
begin
oServer := TsgcHTTPRESTServer.Create(nil);
oServer.Port := 8080;
oServer.OnCommandGet := OnServerCommandGet;
oServer.Active := True;
end;
El manejador utiliza los objetos estándar de petición y respuesta de Indy, así que una respuesta JSON son tres asignaciones:
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 recibe GET y POST. Verbos como PUT, PATCH y DELETE llegan a OnCommandOther, que tiene exactamente la misma firma, de modo que un recurso REST compatible con todos los verbos se suele escribir como una única rutina de despacho invocada desde ambos eventos.
procedure TForm1.OnServerCommandOther(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
if ARequestInfo.Command = 'DELETE' then
begin
AResponseInfo.ResponseNo := 204;
AResponseInfo.ContentText := '';
end;
end;
CORS, desactivado por defecto a propósito
Un navegador que llama a su API desde otro origen necesita las cabeceras Access-Control-* y necesita una respuesta al preflight OPTIONS antes de enviar la petición real. CORSOptions se encarga de las dos cosas.
Lo único que conviene recalcar es que Enabled vale False por defecto y la idea es que siga así salvo que realmente lo necesite. Un servidor que, tras una actualización, empezara a responder en silencio con Access-Control-Allow-Origin: * sería una regresión de seguridad, así que CORS hay que activarlo siempre de forma explícita.
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';
Con esto configurado, el preflight se responde automáticamente con un 204 y las tres cabeceras, y a cada respuesta normal se le añaden esas cabeceras. No hace falta escribir una rama OPTIONS en su manejador.
Prefiera un origen explícito antes que * siempre que la API esté autenticada. Un origen comodín junto con credenciales es una combinación que los navegadores rechazan de todos modos, y enumerar los orígenes a los que realmente sirve es la opción más segura por defecto.
Salud y métricas sin escribirlas
Asigne un componente TsgcHTTPServerStats a la propiedad ServerStats y el servidor podrá responder a dos endpoints operativos. Ambos están desactivados hasta que los habilite individualmente.
oStats := TsgcHTTPServerStats.Create(nil);
oStats.Endpoints.Health.Enabled := True;
oStats.Endpoints.Metrics.Enabled := True;
oServer.ServerStats := oStats;
/health responde con un pequeño documento JSON apto para la sonda de un balanceador de carga:
{"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 responde en el formato de exposición de texto de Prometheus, de modo que se puede recolectar sin ningún adaptador de por medio:
# 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
Ambas rutas son configurables si /metrics y /health chocan con sus propias rutas:
oStats.Endpoints.Metrics.Path := '/internal/metrics';
oStats.Endpoints.Health.Path := '/internal/health';
Estos endpoints se sirven después de la barrera de autenticación, así que heredan la autenticación que el servidor ya aplique. Nunca son públicos salvo que el propio servidor lo sea. Conviene tenerlo en cuenta en los dos sentidos: los mantiene privados por defecto y significa que un recolector de monitorización necesitará credenciales cuando el servidor las exija.
Servir contenido estático junto a la API
DocumentRoot se hereda de TsgcHTTPServer y sigue funcionando, así que un solo servidor puede alojar un pequeño front end y la API con la que se comunica. Todo lo que su manejador no responda cae hacia el directorio de documentos.
oServer.DocumentRoot := 'C:\www\app';
oServer.HTTPCompression.Enabled := True;
oServer.HTTPCompression.MinSize := 1024;
TLS
No cambia nada respecto al servidor base. Active SSL y rellene SSLOptions como siempre:
oServer.SSL := True;
oServer.SSLOptions.Port := 443;
oServer.Port := 443;
Qué viene a continuación
El componente también publica una propiedad Tenancy y una propiedad Tenant de solo lectura, que convierten un único servidor en uno multicliente, y las opciones de Authentication aceptan un componente de almacén de usuarios para que no tenga que guardar credenciales en un TStringList. Todo eso se trata en el segundo artículo, y el tercero muestra cómo poner un contrato OpenAPI delante de todo el conjunto para que las rutas, la validación y la documentación salgan de un mismo archivo.
TsgcHTTPRESTServer ya está disponible. Descargue la última versión desde la página de descargas de sgcWebSockets.
