TsgcHTTPRESTServer: 新しい REST サーバーコンポーネント | eSeGeCe ブログ

TsgcHTTPRESTServer: 新しい REST サーバーコンポーネント

· コンポーネント
sgcWebSockets TsgcHTTPRESTServer component

TsgcHTTPServer の上に REST API を構築することは以前から可能でしたが、毎回手作業で書かなければならないものがいくつかありました。CORS プリフライトへの応答、ロードバランサー用のヘルスエンドポイント、監視スタック用のメトリクスエンドポイント、そしてある顧客のデータを別の顧客のデータと区別する仕組みです。TsgcHTTPRESTServer は、それらすべてを最初から備えた新しいコンポーネントです。

このコンポーネントは TsgcHTTPServer を直接継承しているため、すでに知っている内容はそのまま通用します。同じ Port、同じ SSLOptions、同じ Authentication、同じ OnCommandGet ハンドラーです。TsgcHTTPServer はこれまでどおり素の HTTP サーバーのままで、変更はありません。追加機能は派生クラス側にあり、それぞれがオプトインです。

はじめに

コンポーネントは sgcHTTP_REST_Server ユニットにあり、SGC REST パレットページに TsgcHTTPRESTServer として登録されています。フォームに配置して Active を設定すれば、動作する HTTP サーバーになります。興味深いのはリクエストハンドラーの部分です。

uses
  sgcHTTP_REST_Server;

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

ハンドラーは標準の Indy のリクエストオブジェクトとレスポンスオブジェクトを使うため、JSON の応答は 3 つの代入で書けます。

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 は GET と POST を受け取ります。PUT、PATCH、DELETE などのメソッドは OnCommandOther に届きます。シグネチャはまったく同じなので、すべてのメソッドをサポートする REST リソースは、通常は両方のイベントから呼び出される 1 つのディスパッチルーチンとして書きます。

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

CORS は意図的にデフォルトで無効

別のオリジンから API を呼び出すブラウザーには Access-Control-* ヘッダーが必要であり、実際のリクエストを送る前に OPTIONS プリフライトへの応答も必要です。CORSOptions はその両方を処理します。

強調しておきたいのは、Enabled がデフォルトで False であり、実際に必要になるまではそのままにしておくべきだという点です。アップグレード後にサーバーが黙って Access-Control-Allow-Origin: * を返し始めるとしたら、それはセキュリティの後退です。そのため CORS は厳密にオプトインになっています。

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

これを設定すると、プリフライトには 204 と 3 つのヘッダーで自動的に応答し、通常のレスポンスにもすべてヘッダーが追加されます。ハンドラーに OPTIONS の分岐を書く必要はありません。

API に認証がある場合は、* ではなく明示的なオリジンを指定してください。ワイルドカードのオリジンと認証情報の組み合わせは、いずれにせよブラウザーが拒否します。実際に配信するオリジンを列挙するほうが安全なデフォルトです。

自分で書かずに済むヘルスとメトリクス

TsgcHTTPServerStats コンポーネントを ServerStats プロパティに割り当てると、サーバーは 2 つの運用エンドポイントに応答できるようになります。どちらも個別に有効にするまでは無効のままです。

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

/health は、ロードバランサーのプローブに適した小さな JSON ドキュメントを返します。

{"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 は Prometheus のテキスト公開形式で応答するため、間にアダプターを挟むことなくスクレイプできます。

# 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

/metrics/health が自分のルートと衝突する場合は、どちらのパスも変更できます。

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

これらのエンドポイントは認証ゲートので処理されるため、サーバーがすでに適用している認証をそのまま引き継ぎます。サーバー自体が公開されていない限り、これらが公開されることはありません。この点は両方向で知っておく価値があります。デフォルトで非公開に保たれる一方で、サーバーが認証を要求する場合は監視のスクレイパーにも認証情報が必要になります。

API と並べて静的コンテンツを配信する

DocumentRootTsgcHTTPServer から継承されており、これまでどおり動作します。そのため 1 台のサーバーで、小さなフロントエンドとそれが呼び出す API の両方をホストできます。ハンドラーが応答しなかったものは、そのままドキュメントルートに引き渡されます。

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

TLS

基底のサーバーから変わる点はありません。これまでどおり SSL を設定し、SSLOptions を指定してください。

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

次に続くもの

このコンポーネントは Tenancy プロパティと読み取り専用の Tenant プロパティも公開しており、1 台のサーバーを複数顧客向けのサーバーに変えられます。また Authentication のオプションはユーザーストアコンポーネントを受け付けるため、認証情報を TStringList に保持する必要はありません。これらは2 番目の記事で扱います。3 番目の記事では、全体の前面に OpenAPI コントラクトを置き、ルーティングも検証もドキュメントもすべて 1 つのファイルから得る方法を紹介します。

TsgcHTTPRESTServer はすでに利用できます。最新のビルドは sgcWebSockets のダウンロードページから入手してください。