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に分けます。
-- 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へ入れる方針にします。
-- 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 を通常系と例外系の両方で安全に呼ぶ形にします。
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 を読み切りとして扱います。
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送信では注意が必要です。
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_OBJECT や JSON_ARRAYAGG を使うと安全です。JSON関数全体は Oracle JSON完全ガイド、JSON_TABLEでの取り込みは JSON_TABLEでJSONを取り込む方法 も確認してください。
Bearerトークンを付ける
OAuth 2.0やAPIトークンを使う場合は、Authorization ヘッダーへ Bearer を付与します。トークンはコードに直書きせず、権限を絞った表やWallet、外部の秘密情報管理から取得する設計にします。
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 として返るのが基本です。つまり、通信できたが業務的には失敗、という状態を例外とは別に判定します。
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;
BLOBとしてダウンロードする
PDFや画像などのバイナリレスポンスは、READ_RAW で読み取り、BLOBへ書き込みます。CLOBとして読むと壊れるため、Content-Typeや用途に応じてCLOB/BLOBを分けます。
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も確認します。
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を統一できます。
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ステータス、処理時間、レスポンス先頭数百文字、エラーコード程度に絞ります。
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完全ガイド、監査・追跡ログは ロギングとトレーサビリティを両立する監査設計 が参考になります。
よくあるエラーと切り分け
本番前チェックリスト
connect と resolve を正しく付与しているか。FINALLY ではなくPL/SQLの例外ブロックで END_RESPONSE しているか。まとめ
UTL_HTTP を使うと、PL/SQLからREST APIを呼び出せます。本番で安定させるには、ACL、Wallet、タイムアウト、HTTPステータス判定、JSON処理、ログ、リトライ、そして END_RESPONSE の確実な後始末が重要です。
特に、PL/SQLには FINALLY がない点、resolve 権限はポート範囲なしで付与する点、Content-Lengthはバイト長として扱う点は見落としやすいところです。個別処理にUTL_HTTPを散らばらせず、共通ラッパーで安全な作法を固定すると、運用しやすいAPI連携になります。

