Skip to content

httpClient と外部 API 呼び出しの落とし穴 ​

第7版作成 最終更新 (日本時間)
確認バージョン1.4.17.11.5.7.01.5.8.1

サーバースクリプトから外部 API を呼ぶときに詰まりやすい点をまとめます。多くは「エラーにならないまま静かに間違う」タイプです。要点は次の 3 つです。

  • httpClient は、1 回の実行にまとめられるサーバースクリプトの間で同じインスタンスが共有されます。使う直前に各プロパティを毎回設定し直します(1.4.17.1 までは ResponseHeaders.Clear() も必要でした)。
  • httpClient は multipart/form-data を送れません。
  • 原因が分からないときは、まず SysLogs テーブルを見ます。

落とし穴の一覧 ​

#落とし穴ひとことで
1スクリプトのタイムアウト既定 10 秒。Script.json で延ばす
2ResponseHeaders の重複1.4.17.1 までは 2 回目以降で例外。1.5.8.1 では自動でクリアされる
3httpClient の状態共有使う直前に毎回全部設定し直す
4multipart 非対応送れない。JSON で送れる API を選ぶ
5items.Get() の戻り値.length ではなく .Length
6ChoiceHash読めない(設定専用)
7拡張 SQL"Api": true 必須。無いと NullReference
8AfterUpdate の saved更新後の値。context.UserData で橋渡し
9通知の HttpClient 型使えない。Slack / Teams 型を使う
10Send() の戻り値常に true。あてにしない
11エラーの調べ方SysLogs テーブルを見る

以下、1.5.7.0 を対象に確認した内容です(2 のみ 1.4.17.1 までを対象にした内容)。httpClient と通知の実装(2・3・9・10)は 確認したソースでも確認しています。

httpClient の基本の書き方 ​

以下の 1〜4 を踏まえると、httpClient は次の形で使うのが安全です。

js
httpClient.ResponseHeaders.Clear();     // 1.4.17.1 以前向け(1.5.8.1 では不要だが害はない)
httpClient.RequestUri = ENDPOINT;
httpClient.RequestHeaders.Clear();      // ヘッダも残っている
httpClient.MediaType = 'application/json';
httpClient.Encoding = 'utf-8';          // 明示的に戻す
httpClient.TimeOut = 45000;
httpClient.Content = body;
httpClient.Post();                      // Get / Put / Delete / Patch なども同様

1. サーバースクリプトのタイムアウトは既定 10 秒 ​

App_Data/Parameters/Script.json の既定値は次のとおりです。

json
{
    "ServerScriptTimeOut": 10000,
    "ServerScriptTimeOutChangeable": false,
    "ServerScriptHttpClientTimeOut": 100000
}

httpClient 側は 100 秒あっても、それを包むサーバースクリプトが 10 秒で打ち切られます。生成 AI のように応答に数秒〜十数秒かかる相手だと、成功したり失敗したりします。

ServerScriptTimeOut を延ばし、ServerScriptTimeOutChangeable を true にするとスクリプト単位でも指定できます。変更後は再起動が必要です。

json
{
    "ServerScriptTimeOut": 60000,
    "ServerScriptTimeOutChangeable": true
}

スクリプト単位のタイムアウトが実際にはどう適用されるかは タイムアウト を参照してください。

WARNING

DisableServerScriptHttpClient が true の環境では httpClient 自体が使えません。既定は false です。

2. 2 回目以降の呼び出しで例外になる(ResponseHeaders) 〜 1.4.17.1 ​

同じ条件で httpClient を 2 回以上実行すると、2 回目以降でエラーになります。リクエストを組み立てる前に ResponseHeaders をクリアすれば回避できます。

js
httpClient.ResponseHeaders.Clear();
//RequestUri、RequestHeadersやMediaTypeのセット
httpClient.Post();//他のGet/Put/Delete/Patchなども同様

原因は次のとおりです。

  • 同じ実行タイミングのサーバースクリプトは文字列結合されてひとまとまりで実行され、その間 httpClient のインスタンスは使い回されます。
  • httpClient は取得したヘッダを ResponseHeaders(Dictionary)に foreach で追加していきますが、前回の結果を消していません。Date など毎回返るキーが重複し、例外になります。

該当箇所: ServerScriptModelHttpClient.cs#L31-L34

1.5.8.1 では修正済み

この内容は 1.4.17.1 までを対象にしたものです。確認したソースでは、Get() / Post() などの送信処理の最初で ResponseHeaders.Clear() が呼ばれるため、2 回目以降の呼び出しでもキーは重複せず、この例外は起きません(ServerScriptModelHttpClient.cs)。古い版と同じスクリプトを使う場合に備えて ResponseHeaders.Clear() を書いておいても害はありません(履歴の自動削除 などの例もこの書き方です)。

3. httpClient はスクリプト間で共有される ​

httpClient は、同じタイミング(条件)で 1 回の実行にまとめられるサーバースクリプトが同じインスタンスを共有します。あるスクリプトが変えた設定を、次に走るスクリプトが引き継ぎます。

確認したソースでは、httpClient のインスタンスはサーバースクリプトの実行 1 回ごとに ServerScriptModel の中で作られます(ServerScriptModel.cs、ServerScriptUtilities.cs)。同じ条件の複数のスクリプトは本文が連結されて 1 回で実行されるため、その間は同じインスタンスです。一方、「更新前」と「更新後」のように別の条件の実行では別のインスタンスになります。

たとえば、あるスクリプトが httpClient.Encoding = 'iso-8859-1'; としたまま終わると、次に走るスクリプトが日本語を iso-8859-1 で送信してしまいます。HTTP 200 が返りエラーにはならないため、気づきにくい不具合になります(実例では、文字化けしたテキストのベクトルが返り、類似度の計算結果だけがおかしくなりました)。

対策は、使う直前に毎回すべて設定し直すことです(上の「基本の書き方」を参照)。RequestHeaders も同様で、前のスクリプトが付けた Authorization が残っていると、別のサービスに他所のトークンを送ることになりかねません。RequestHeaders.Clear() は必ず呼んでください。

4. multipart/form-data を送れない ​

httpClient.Content は文字列で、内部的には StringContent として送信されます。MediaType に boundary 付きの値を代入することはできますが、送信時に失敗します。

js
httpClient.MediaType = 'multipart/form-data; boundary=----abc123'; // 代入は成功する
httpClient.Content = body;
httpClient.Post();
// → The format of value 'multipart/form-data; boundary=----abc123' is invalid.

StringContent のメディアタイプにはパラメータ(; boundary=...)を含められないためです。multipart を要求する API(例: OpenAI 互換の /v1/audio/transcriptions 系の音声文字起こし)はサーバースクリプトから直接呼べません。回避策は次のとおりです。

  • JSON で送れる API を選ぶ(画像なら data URL で JSON に埋め込める)
  • プリザンターの外に出す(API を呼ぶ常駐ワーカーを別に用意し、プリザンターの API 経由でやりとりする)

5. items.Get() の戻り値は .Length ​

items.Get(siteId) が返すのは .NET の配列で、JavaScript の配列ではありません。

js
var rows = items.Get(context.SiteId);

// ❌ undefined。ループが 1 回も回らないが、エラーにもならない
for (var i = 0; i < rows.length; i++) { }

// ✅
for (var i = 0; i < rows.Length; i++) { }

rows.length は undefined なので 0 < undefined が false になり、何事もなかったように処理が終わります。同じ理由で rows.forEach(...) や rows.map(...) も使えません。「0 件しか見つからない」ときはここを疑ってください。

6. columns.X.ChoiceHash は読めない ​

分類項目の選択肢をスクリプトから取ろうとしても、ChoiceHash は常に null です。

text
typeof columns = object
columns.ClassA = ok
LabelText = 種別       ← これは読める
ChoiceHash = null      ← これは読めない

ChoiceHash はサーバースクリプトから選択肢を設定するためのプロパティで、現在の選択肢の取得には使えません。選択肢一覧が必要なら、スクリプト側に定義を持つか、拡張 SQL で Sites.SiteSettings(JSON)を読んでパースします。

7. 拡張 SQL は "Api": true が無いと NullReference で落ちる ​

extendedSql.ExecuteRow('MySql', ...) を呼ぶと、次の例外が出ることがあります。

text
System.NullReferenceException: Object reference not set to an instance of an object.
   at Implem.Pleasanter.Models.ExtensionUtilities.DataSetToExpando(DataSet dataSet)
   at Implem.Pleasanter.Models.ExtensionUtilities.ExecuteDataSetAsDynamic(...)
   at Implem.Pleasanter.Libraries.ServerScripts.ServerScriptModelExtendedSql.ExecuteRow(...)

原因は SQL や接続ではなく、拡張 SQL の定義に "Api": true が無いことです。

json
{
  "Name": "GetLatestImage",
  "Api": true,
  "SiteIdList": [5],
  "CommandText": "select ..."
}

サーバースクリプトから呼べるのは Api が true の拡張 SQL だけで、条件に合わない場合は「見つからない」ではなく null が返り、その先で落ちます。名前の綴り間違いでも同じ例外になります。

8. AfterUpdate では saved が更新後の値になっている ​

「値が変わったときだけ処理する」を書こうとすると、AfterUpdate では saved も更新後の値になっていて差分が取れません。

js
// AfterUpdate での実測
saved.Status = 300
model.Status = 300   ← 更新前の値が取れない

// BeforeUpdate での実測
saved.Status = 300
model.Status = 900   ← 差分が取れる

BeforeUpdate なら差分は取れますが、そこで外部通知まで済ませると、保存が失敗しても通知だけ飛びます。

context.UserData は同一リクエスト内のサーバースクリプト間で共有される入れ物なので、BeforeUpdate で書いて AfterUpdate で読むことができます。

js
if (context.Condition === 'BeforeUpdate') {
  context.UserData.statusBefore = String(saved.Status == null ? '' : saved.Status);
  return;
}

// AfterUpdate
var before = context.UserData.statusBefore;
var after = String(model.Status == null ? '' : model.Status);
if (before !== after) {
  // 保存が完了してから通知する
}

9. 通知の HttpClient 型はサーバースクリプトから使えない ​

notifications で汎用 Webhook(Type = 9)を送ろうとすると、二段階でつまずきます。

  1. 既定で無効です(App_Data/Parameters/Notification.json の "HttpClient": false)。
  2. true にすると、今度は ERR Error: Value cannot be null. (Parameter 'name') という例外になります。HttpClient 型の送信処理は Encoding / MediaType / MethodType / Headers を参照しますが(Notification.cs)、サーバースクリプト側のラッパーはこれらを持っておらず(ServerScriptModelNotificationModel.cs)、Encoding が null のまま Encoding.GetEncoding(null) が呼ばれて落ちます。

Slack 型(Type = 2)や Teams 型(Type = 6)はこれらを使わないので問題なく動きます。Slack 型は Address に {"text": "..."} を POST するだけなので、受け口が Slack でなくても、その形式で受け取れる Webhook なら流用できます。

js
var n = notifications.New();
n.Type = 2;                  // Slack
n.Address = WEBHOOK_URL;
n.Title = 'タイトル';
n.Body = '本文';
n.Send();

10. notifications の Send() は常に true を返す ​

Send() の戻り値は送信結果ではありません。確認したソースでは、送信処理を呼んだあと無条件に true を返しています(ServerScriptModelNotificationModel.cs)。通知タイプが無効化されていても、パラメータで機能が閉じられていても true が返ります。戻り値で成否を判定せず、受け口側で確認してください。開発中は受信を確認できる Webhook を用意しておくと確実です。

11. エラーは SysLogs テーブルで調べる ​

サーバースクリプトが落ちると、画面には次のメッセージしか出ません。

text
サーバスクリプトの実行に失敗しました。
サーバスクリプトを確認・修正してから、再度お試しください。

コンテナのログ(docker logs)にも出ません。スタックトレースは SysLogs テーブルにあります。

sql
select top (3) "SysLogId", "ErrMessage", "ErrStackTrace"
from "SysLogs"
where "ErrMessage" is not null
order by "SysLogId" desc;
sql
select "SysLogId", "ErrMessage", "ErrStackTrace"
from "SysLogs"
where "ErrMessage" is not null
order by "SysLogId" desc
limit 3;
sql
select `SysLogId`, `ErrMessage`, `ErrStackTrace`
from `SysLogs`
where `ErrMessage` is not null
order by `SysLogId` desc
limit 3;

7 の「拡張 SQL の Api 忘れ」のように、ここを見て初めて原因が分かるものがあります。この SQL をすぐ実行できるようにしておくと、調査が大幅に速くなります。

関連ページ ​

変更履歴

第7版記事の確認版を繰り返す表現を整理する
第6版外部 API エラーログの取得 SQL を3種類のDBMSに対応
第5版「機能の仕様と使いこなし」「スクリプト」「サーバースクリプト」に対応バージョンを表示
第4版「サーバースクリプト」を 1.5.8.1 のソースで検証して修正
第3版元記事への言及を整理し、必要なコードをページに収録。検索機能に一覧の検索と絞り込みを追加
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「サーバースクリプト」セクションの記事を追加