TsgcHTTPRESTServer: o novo componente de servidor REST | Blog eSeGeCe

TsgcHTTPRESTServer: o novo componente de servidor REST

· Componentes
sgcWebSockets TsgcHTTPRESTServer component

Construir uma API REST sobre o TsgcHTTPServer sempre foi possível, mas algumas coisas tinham que ser escritas à mão toda vez: a resposta ao preflight CORS, um endpoint de saúde para o balanceador de carga, um endpoint de métricas para a pilha de monitoramento e alguma forma de distinguir os dados de um cliente dos de outro. O TsgcHTTPRESTServer é um novo componente que entrega tudo isso pronto.

Ele descende diretamente do TsgcHTTPServer, então tudo o que você já conhece continua valendo: a mesma Port, as mesmas SSLOptions, a mesma Authentication, o mesmo manipulador OnCommandGet. O TsgcHTTPServer continua sendo um servidor HTTP simples e não muda. Os extras ficam no descendente, e cada um deles é opcional.

Primeiros passos

O componente está na unit sgcHTTP_REST_Server e é registrado na página de paleta SGC REST como TsgcHTTPRESTServer. Ao soltá-lo em um formulário e definir Active você já tem um servidor HTTP funcionando; a parte interessante é o manipulador de requisições.

uses
  sgcHTTP_REST_Server;

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

O manipulador usa os objetos padrão de requisição e resposta do Indy, então uma resposta JSON são três atribuições:

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;

O OnCommandGet recebe GET e POST. Verbos como PUT, PATCH e DELETE chegam em OnCommandOther, que tem exatamente a mesma assinatura, então um recurso REST que suporta o conjunto completo de verbos normalmente é escrito como uma única rotina de despacho chamada a partir dos dois 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, desativado por padrão de propósito

Um navegador que chama a sua API a partir de outra origem precisa dos cabeçalhos Access-Control-*, e precisa de uma resposta ao preflight OPTIONS antes de enviar a requisição real. O CORSOptions cuida das duas coisas.

O ponto que vale destacar é que Enabled é False por padrão e a intenção é que continue assim, a menos que você realmente precise. Um servidor que passasse silenciosamente a responder com Access-Control-Allow-Origin: * depois de uma atualização seria uma regressão de segurança, então o CORS é estritamente opcional.

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

Com isso configurado, o preflight é respondido automaticamente com um 204 e os três cabeçalhos, e toda resposta normal recebe os cabeçalhos adicionados. Você não escreve um ramo OPTIONS no seu manipulador.

Prefira uma origem explícita a * sempre que a API for autenticada. Uma origem curinga com credenciais é uma combinação que os navegadores rejeitam de qualquer forma, e listar as origens que você realmente atende é o padrão mais seguro.

Saúde e métricas sem precisar escrevê-las

Anexe um componente TsgcHTTPServerStats à propriedade ServerStats e o servidor passa a responder dois endpoints operacionais. Ambos ficam desativados até que você os habilite individualmente.

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

/health responde com um pequeno documento JSON adequado para uma sonda de 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 no formato de exposição de texto do Prometheus, de modo que pode ser coletado sem nenhum adaptador no meio:

# 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

Os dois caminhos são configuráveis caso /metrics e /health conflitem com as suas próprias rotas:

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

Esses endpoints são servidos depois do controle de autenticação, então herdam qualquer autenticação que o servidor já imponha. Eles nunca são públicos, a menos que o próprio servidor seja público. Isso vale saber nos dois sentidos: mantém os endpoints privados por padrão, e significa que um coletor de monitoramento precisa de credenciais quando o servidor as exige.

Servindo conteúdo estático junto com a API

DocumentRoot é herdado do TsgcHTTPServer e continua funcionando, então um único servidor pode hospedar um pequeno front end e a API com a qual ele conversa. Tudo o que o seu manipulador não responder cai no document root.

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

TLS

Nada muda em relação ao servidor base. Defina SSL e preencha SSLOptions como de costume:

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

O que vem a seguir

O componente também publica uma propriedade Tenancy e uma propriedade somente leitura Tenant, que transformam um único servidor em um servidor multicliente, e as opções de Authentication aceitam um componente de repositório de usuários, para que você não precise manter credenciais em uma TStringList. Isso é abordado no segundo artigo, e o terceiro mostra como colocar um contrato OpenAPI à frente de tudo isso, para que as rotas, a validação e a documentação venham todas de um único arquivo.

O TsgcHTTPRESTServer já está disponível. Baixe a versão mais recente na página de download do sgcWebSockets.