【PL/SQL】UTL_HTTPでREST API連携する方法|ACL・Wallet・JSON・例外処理まで

【PL/SQL】UTL_HTTPを使ったWeb API連携の実装方法 PL/SQL

UTL_HTTP は、Oracle Database内のPL/SQLからHTTP/HTTPSリクエストを送信するための標準パッケージです。外部REST APIの呼び出し、社内API連携、JSON取得、ファイルダウンロード、ジョブ結果通知などをデータベース側で実装できます。

ただし、UTL_HTTP はネットワーク、証明書、認証、文字コード、HTTPステータス、レスポンス後始末が絡むため、サンプルをそのまま貼るだけでは本番で詰まりやすい領域です。この記事では、PL/SQLで安全にREST API連携するための実装テンプレートに寄せて解説します。網羅的な仕様確認は Oracle UTL_HTTP完全ガイド、JSONレスポンスの展開は JSON_TABLEでJSONを取り込む方法 も参考になります。

この記事で扱うこと

  • ネットワークACLとWalletの準備
  • GET / POST の基本テンプレート
  • PL/SQLに存在しない FINALLY を使わない後始末
  • JSON送受信、Bearerトークン、Content-Lengthの注意
  • CLOB/BLOBレスポンス、ファイル保存、ログ設計
  • HTTPステータス、タイムアウト、TLS、プロキシ、リトライ設計
スポンサーリンク

前提:ACLで外部接続を許可する

Oracle Databaseから外部HTTPサーバへ接続するには、対象ホストへのネットワークACLが必要です。HTTPSの443番へ接続するなら、対象スキーマに connect 権限を付与します。名前解決に resolve を付ける場合、Oracle公式の注意どおり、ポート範囲なしのACEに分けます。

network-acl.sql
-- DBA権限で実行する例
BEGIN
  -- 443番ポートへの接続を許可
  DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
    host       => 'api.example.com',
    lower_port => 443,
    upper_port => 443,
    ace        => xs$ace_type(
      privilege_list => xs$name_list('connect'),
      principal_name => 'APP_USER',
      principal_type => xs_acl.ptype_db
    )
  );

  -- 名前解決権限。resolveはポート範囲を指定しない
  DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
    host => 'api.example.com',
    ace  => xs$ace_type(
      privilege_list => xs$name_list('resolve'),
      principal_name => 'APP_USER',
      principal_type => xs_acl.ptype_db
    )
  );
END;
/

GRANT EXECUTE ON UTL_HTTP TO APP_USER;

ORA-24247 が出る場合は、ACL不足、接続ユーザー違い、PDB違い、ホスト名違い、プロキシ経由の接続先違いを確認します。UTL_MAILやUTL_SMTPでも同じACLの考え方を使います。メール連携の例は UTL_MAIL・UTL_SMTPでメール送信する方法 も参考になります。

HTTPSではWalletを準備する

HTTPSへ接続する場合、サーバ証明書を検証するためにOracle Walletを設定します。自己署名証明書や社内CAを使う場合も、検証を回避するのではなく、信頼するCA証明書をWalletへ入れる方針にします。

wallet-setup.sql
-- PL/SQL側ではWalletのパスを指定する
BEGIN
  UTL_HTTP.SET_WALLET(
    path     => 'file:/u01/app/oracle/wallets/api',
    password => 'wallet_password'
  );
END;
/

WalletはDBサーバ上のファイルです。OS権限、Walletパス、証明書期限、プロキシでTLS終端しているかを事前に確認します。証明書検証に失敗すると ORA-29024 などで止まることがあります。

PL/SQLにFINALLYはない

UTL_HTTPの記事でよくある落とし穴が、他言語のように FINALLY を書いてしまうことです。PL/SQLには FINALLY 節がないため、END_RESPONSE を通常系と例外系の両方で安全に呼ぶ形にします。

safe-end-response-pattern.sql
DECLARE
  l_req       UTL_HTTP.req;
  l_resp      UTL_HTTP.resp;
  l_resp_open BOOLEAN := FALSE;
BEGIN
  l_req := UTL_HTTP.BEGIN_REQUEST('https://api.example.com/health', 'GET');
  l_resp := UTL_HTTP.GET_RESPONSE(l_req);
  l_resp_open := TRUE;

  DBMS_OUTPUT.PUT_LINE('status=' || l_resp.status_code);

  UTL_HTTP.END_RESPONSE(l_resp);
  l_resp_open := FALSE;
EXCEPTION
  WHEN OTHERS THEN
    IF l_resp_open THEN
      BEGIN
        UTL_HTTP.END_RESPONSE(l_resp);
      EXCEPTION
        WHEN OTHERS THEN NULL;
      END;
    END IF;
    RAISE;
END;
/

レスポンス取得前に例外が発生することもあります。そのため、未初期化の resp.status_code を例外ブロックで直接参照しないようにします。状態は l_resp_open のようなフラグで管理すると安全です。

GETでJSONを取得する

GETでは、リクエスト作成、ヘッダー設定、レスポンス取得、本文読み取り、レスポンス終了という流れになります。本文はCLOBへ積み上げ、UTL_HTTP.END_OF_BODY を読み切りとして扱います。

get-json-clob.sql
CREATE OR REPLACE FUNCTION http_get_json(
  p_url IN VARCHAR2
) RETURN CLOB
AS
  l_req       UTL_HTTP.req;
  l_resp      UTL_HTTP.resp;
  l_resp_open BOOLEAN := FALSE;
  l_chunk     VARCHAR2(32767);
  l_body      CLOB;
BEGIN
  DBMS_LOB.CREATETEMPORARY(l_body, TRUE);

  UTL_HTTP.SET_TRANSFER_TIMEOUT(15000);
  UTL_HTTP.SET_WALLET('file:/u01/app/oracle/wallets/api', 'wallet_password');

  l_req := UTL_HTTP.BEGIN_REQUEST(p_url, 'GET', 'HTTP/1.1');
  UTL_HTTP.SET_HEADER(l_req, 'Accept', 'application/json');
  UTL_HTTP.SET_HEADER(l_req, 'User-Agent', 'Oracle-UTL_HTTP');

  l_resp := UTL_HTTP.GET_RESPONSE(l_req);
  l_resp_open := TRUE;

  LOOP
    BEGIN
      UTL_HTTP.READ_TEXT(l_resp, l_chunk, 32767);
      DBMS_LOB.WRITEAPPEND(l_body, LENGTH(l_chunk), l_chunk);
    EXCEPTION
      WHEN UTL_HTTP.END_OF_BODY THEN
        EXIT;
    END;
  END LOOP;

  UTL_HTTP.END_RESPONSE(l_resp);
  l_resp_open := FALSE;

  RETURN l_body;
EXCEPTION
  WHEN OTHERS THEN
    IF l_resp_open THEN
      BEGIN
        UTL_HTTP.END_RESPONSE(l_resp);
      EXCEPTION
        WHEN OTHERS THEN NULL;
      END;
    END IF;
    RAISE;
END;
/

CLOBを扱う処理では、文字数とバイト数、temporary LOBの解放、レスポンスサイズに注意します。LOB処理の基本は DBMS_LOB完全ガイド が参考になります。

POSTでJSONを送信する

POSTでは、JSON本文を書き込んでから GET_RESPONSE を呼びます。Content-Typeは application/json; charset=UTF-8 を明示します。Content-Length はHTTP上はバイト長なので、日本語を含むJSONやCLOB送信では注意が必要です。

post-json.sql
DECLARE
  l_req       UTL_HTTP.req;
  l_resp      UTL_HTTP.resp;
  l_resp_open BOOLEAN := FALSE;
  l_body      CLOB;
  l_chunk     VARCHAR2(32767);
BEGIN
  SELECT JSON_OBJECT(
           'email' VALUE 'user@example.com',
           'name'  VALUE 'Sato'
           RETURNING CLOB
         )
  INTO l_body
  FROM dual;

  UTL_HTTP.SET_TRANSFER_TIMEOUT(15000);
  UTL_HTTP.SET_WALLET('file:/u01/app/oracle/wallets/api', 'wallet_password');

  l_req := UTL_HTTP.BEGIN_REQUEST('https://api.example.com/v1/users', 'POST', 'HTTP/1.1');
  UTL_HTTP.SET_HEADER(l_req, 'Content-Type', 'application/json; charset=UTF-8');
  UTL_HTTP.SET_HEADER(l_req, 'Accept', 'application/json');
  UTL_HTTP.SET_HEADER(l_req, 'User-Agent', 'Oracle-UTL_HTTP');

  -- ASCII中心の短いJSONなら文字数=バイト数になりやすいが、
  -- 日本語を含む本文ではバイト長の扱いを個別に検証する
  UTL_HTTP.WRITE_TEXT(l_req, l_body);

  l_resp := UTL_HTTP.GET_RESPONSE(l_req);
  l_resp_open := TRUE;

  DBMS_OUTPUT.PUT_LINE('status=' || l_resp.status_code || ' ' || l_resp.reason_phrase);

  LOOP
    BEGIN
      UTL_HTTP.READ_TEXT(l_resp, l_chunk, 32767);
      DBMS_OUTPUT.PUT_LINE(SUBSTR(l_chunk, 1, 1000));
    EXCEPTION
      WHEN UTL_HTTP.END_OF_BODY THEN
        EXIT;
    END;
  END LOOP;

  UTL_HTTP.END_RESPONSE(l_resp);
  l_resp_open := FALSE;
EXCEPTION
  WHEN OTHERS THEN
    IF l_resp_open THEN
      BEGIN
        UTL_HTTP.END_RESPONSE(l_resp);
      EXCEPTION
        WHEN OTHERS THEN NULL;
      END;
    END IF;
    RAISE;
END;
/

JSON文字列を手で連結するとエスケープ漏れが起きます。JSON_OBJECTJSON_ARRAYAGG を使うと安全です。JSON関数全体は Oracle JSON完全ガイド、JSON_TABLEでの取り込みは JSON_TABLEでJSONを取り込む方法 も確認してください。

Bearerトークンを付ける

OAuth 2.0やAPIトークンを使う場合は、Authorization ヘッダーへ Bearer を付与します。トークンはコードに直書きせず、権限を絞った表やWallet、外部の秘密情報管理から取得する設計にします。

bearer-token.sql
l_req := UTL_HTTP.BEGIN_REQUEST(
           'https://api.example.com/v1/resources',
           'GET',
           'HTTP/1.1'
         );

UTL_HTTP.SET_HEADER(l_req, 'Accept', 'application/json');
UTL_HTTP.SET_HEADER(l_req, 'Authorization', 'Bearer ' || l_access_token);

401が返ったときに即再試行する場合でも、再取得、再送、失敗ログ、再試行回数の上限を決めます。POSTの再送は副作用があるため、API側に冪等キーがあるか確認してから実装します。

HTTPステータスと例外を分けて扱う

ネットワーク障害やタイムアウトはPL/SQL例外になり得ますが、HTTPの4xx/5xxはレスポンスの status_code として返るのが基本です。つまり、通信できたが業務的には失敗、という状態を例外とは別に判定します。

status-handling.sql
IF l_resp.status_code BETWEEN 200 AND 299 THEN
  -- success
  NULL;
ELSIF l_resp.status_code IN (429, 500, 502, 503, 504) THEN
  -- retry candidate
  RAISE_APPLICATION_ERROR(-20050, 'retryable http status: ' || l_resp.status_code);
ELSE
  -- client error or business error
  RAISE_APPLICATION_ERROR(-20051, 'http status: ' || l_resp.status_code);
END IF;
2xx成功。レスポンスJSONを検証して処理します。
400/422リクエスト不正。JSONや必須項目を見直します。
401/403認証・認可エラー。トークン、権限、期限を確認します。
404URL、パス、APIバージョンを確認します。
429/5xxレート制限やサーバ障害。上限付きリトライを検討します。

BLOBとしてダウンロードする

PDFや画像などのバイナリレスポンスは、READ_RAW で読み取り、BLOBへ書き込みます。CLOBとして読むと壊れるため、Content-Typeや用途に応じてCLOB/BLOBを分けます。

download-blob.sql
DECLARE
  l_req       UTL_HTTP.req;
  l_resp      UTL_HTTP.resp;
  l_resp_open BOOLEAN := FALSE;
  l_raw       RAW(32767);
  l_blob      BLOB;
BEGIN
  DBMS_LOB.CREATETEMPORARY(l_blob, TRUE);

  l_req := UTL_HTTP.BEGIN_REQUEST('https://api.example.com/files/report.pdf', 'GET');
  UTL_HTTP.SET_HEADER(l_req, 'Accept', 'application/pdf');

  l_resp := UTL_HTTP.GET_RESPONSE(l_req);
  l_resp_open := TRUE;

  LOOP
    BEGIN
      UTL_HTTP.READ_RAW(l_resp, l_raw, 32767);
      DBMS_LOB.WRITEAPPEND(l_blob, UTL_RAW.LENGTH(l_raw), l_raw);
    EXCEPTION
      WHEN UTL_HTTP.END_OF_BODY THEN
        EXIT;
    END;
  END LOOP;

  UTL_HTTP.END_RESPONSE(l_resp);
  l_resp_open := FALSE;

  INSERT INTO api_file_store(file_id, content_type, content_blob, created_at)
  VALUES (api_file_seq.NEXTVAL, 'application/pdf', l_blob, SYSDATE);
EXCEPTION
  WHEN OTHERS THEN
    IF l_resp_open THEN
      BEGIN
        UTL_HTTP.END_RESPONSE(l_resp);
      EXCEPTION
        WHEN OTHERS THEN NULL;
      END;
    END IF;
    RAISE;
END;
/

DB外のファイルとして保存するなら UTL_FILE も候補です。ファイル出力の実務パターンは UTL_FILEでCSV・ログファイルを読み書きする方法 で扱っています。

プロキシとリダイレクト

社内ネットワークでは、外部APIへの接続がプロキシ必須になっていることがあります。プロキシ経由の場合は、APIホストだけでなくプロキシホストへのACLも確認します。

proxy-and-redirect.sql
BEGIN
  UTL_HTTP.SET_PROXY('http://proxy.example.local:8080');

  -- リダイレクト追跡が必要な場合は環境方針に合わせて設定
  UTL_HTTP.SET_FOLLOW_REDIRECT(3);
END;
/

リダイレクトを自動追跡する場合、HTTPからHTTPSへの移動、別ドメインへの移動、認証ヘッダーの扱いを確認します。セキュリティ上、想定外ドメインへのリダイレクトを許すべきでないケースもあります。

共通ラッパーパッケージを作る

本番では、個別処理ごとにUTL_HTTPを直接書くより、薄いラッパーパッケージを用意する方が安全です。タイムアウト、Wallet、User-Agent、例外処理、ログ、END_RESPONSEを統一できます。

http-client-package.sql
CREATE OR REPLACE PACKAGE http_client AS
  FUNCTION get_json(
    p_url    IN VARCHAR2,
    p_token  IN VARCHAR2 DEFAULT NULL
  ) RETURN CLOB;
END http_client;
/

CREATE OR REPLACE PACKAGE BODY http_client AS
  PROCEDURE setup IS
  BEGIN
    UTL_HTTP.SET_TRANSFER_TIMEOUT(15000);
    UTL_HTTP.SET_WALLET('file:/u01/app/oracle/wallets/api', 'wallet_password');
    UTL_HTTP.SET_DETAILED_EXCP_SUPPORT(TRUE);
  END;

  FUNCTION get_json(
    p_url    IN VARCHAR2,
    p_token  IN VARCHAR2 DEFAULT NULL
  ) RETURN CLOB
  AS
    l_req       UTL_HTTP.req;
    l_resp      UTL_HTTP.resp;
    l_resp_open BOOLEAN := FALSE;
    l_chunk     VARCHAR2(32767);
    l_body      CLOB;
  BEGIN
    setup;
    DBMS_LOB.CREATETEMPORARY(l_body, TRUE);

    l_req := UTL_HTTP.BEGIN_REQUEST(p_url, 'GET', 'HTTP/1.1');
    UTL_HTTP.SET_HEADER(l_req, 'Accept', 'application/json');
    UTL_HTTP.SET_HEADER(l_req, 'User-Agent', 'Oracle-UTL_HTTP');

    IF p_token IS NOT NULL THEN
      UTL_HTTP.SET_HEADER(l_req, 'Authorization', 'Bearer ' || p_token);
    END IF;

    l_resp := UTL_HTTP.GET_RESPONSE(l_req);
    l_resp_open := TRUE;

    LOOP
      BEGIN
        UTL_HTTP.READ_TEXT(l_resp, l_chunk, 32767);
        DBMS_LOB.WRITEAPPEND(l_body, LENGTH(l_chunk), l_chunk);
      EXCEPTION
        WHEN UTL_HTTP.END_OF_BODY THEN
          EXIT;
      END;
    END LOOP;

    UTL_HTTP.END_RESPONSE(l_resp);
    l_resp_open := FALSE;

    RETURN l_body;
  EXCEPTION
    WHEN OTHERS THEN
      IF l_resp_open THEN
        BEGIN
          UTL_HTTP.END_RESPONSE(l_resp);
        EXCEPTION
          WHEN OTHERS THEN NULL;
        END;
      END IF;
      RAISE;
  END get_json;
END http_client;
/

パッケージにまとめるとテストもしやすくなります。テスト容易性やモック化の考え方は テスト容易性を高めるプロシージャ設計とモック化手法 と相性が良いです。

ログとジョブ連携

API連携は、失敗時の原因調査ができるようにログを残します。ただし、アクセストークン、個人情報、レスポンス全文をそのままログに残すのは危険です。URL、HTTPステータス、処理時間、レスポンス先頭数百文字、エラーコード程度に絞ります。

api-call-log.sql
CREATE TABLE api_call_log (
  log_id        NUMBER GENERATED ALWAYS AS IDENTITY,
  api_name      VARCHAR2(100),
  request_url   VARCHAR2(1000),
  http_status   NUMBER,
  elapsed_ms    NUMBER,
  result_status VARCHAR2(20),
  message       VARCHAR2(1000),
  created_at    TIMESTAMP DEFAULT SYSTIMESTAMP
);

定期的なAPI取得は DBMS_SCHEDULER と組み合わせます。ジョブ管理は DBMS_SCHEDULER完全ガイド、監査・追跡ログは ロギングとトレーサビリティを両立する監査設計 が参考になります。

よくあるエラーと切り分け

ORA-24247ACL不足。APIホスト、プロキシホスト、ポート、実行ユーザーを確認します。
ORA-29024証明書検証失敗。Wallet、CA証明書、証明書期限、SNI/プロキシを確認します。
TRANSFER_TIMEOUTタイムアウト。API遅延、FW、プロキシ、DNS、タイムアウト値を確認します。
REQUEST_FAILED詳細例外サポートを有効にして原因をログ化します。
401/403トークン期限、スコープ、APIキー、認可設定を確認します。
文字化けContent-Type、charset、WRITE_TEXT/READ_TEXT、バイト長を確認します。

本番前チェックリスト

ACLconnectresolve を正しく付与しているか。
WalletCA証明書、パス、OS権限、期限切れを確認したか。
後始末FINALLY ではなくPL/SQLの例外ブロックで END_RESPONSE しているか。
認証トークンを直書きせず、権限を絞った保管方式にしているか。
再試行冪等性、最大回数、待機時間、重複実行の副作用を決めたか。
ログHTTPステータス、処理時間、エラー、レスポンス抜粋を残すか。
機微情報トークン、個人情報、レスポンス全文をログに残していないか。
JSON文字列連結ではなくJSON関数を使い、レスポンス検証をしているか。

まとめ

UTL_HTTP を使うと、PL/SQLからREST APIを呼び出せます。本番で安定させるには、ACL、Wallet、タイムアウト、HTTPステータス判定、JSON処理、ログ、リトライ、そして END_RESPONSE の確実な後始末が重要です。

特に、PL/SQLには FINALLY がない点、resolve 権限はポート範囲なしで付与する点、Content-Lengthはバイト長として扱う点は見落としやすいところです。個別処理にUTL_HTTPを散らばらせず、共通ラッパーで安全な作法を固定すると、運用しやすいAPI連携になります。