はじめに
プリザンターのサーバスクリプトから外部 API を叩く記事をいくつか書きました。
実装そのものは短いのですが、書いている時間より詰まっている時間のほうが長かったというのが正直なところです。
この記事は、その過程で踏んだ落とし穴を 10 個まとめたものです。
どれも「エラーメッセージから原因が分からない」「そもそもエラーにならない」タイプで、
知らないと確実に時間を溶かします。
対象は httpClient・items・extendedSql・notifications あたりです。
特定の外部サービスに依存する話はほとんどありません。
バージョン 1.5.7.0 を対象にしています
1. サーバスクリプトのタイムアウトは既定10秒
いちばん最初に踏みます。App_Data/Parameters/Script.json の既定値です。
{
"ServerScriptTimeOut": 10000,
"ServerScriptTimeOutChangeable": false,
"ServerScriptHttpClientTimeOut": 100000
}
httpClient 側は 100 秒あるのに、それを包んでいるサーバスクリプトが 10 秒で打ち切られます。
生成 AI のように応答が数秒〜十数秒かかる相手だと、成功したり失敗したりする気持ちの悪い挙動になります。
ServerScriptTimeOut を延ばし、ServerScriptTimeOutChangeable を true にすると
スクリプト単位でも指定できるようになります。変更後は再起動が必要です。
{
"ServerScriptTimeOut": 60000,
"ServerScriptTimeOutChangeable": true
}
DisableServerScriptHttpClient が true の環境では httpClient 自体が使えません。
既定は false ですが、先に確認しておくと安心です。
2. httpClient はスクリプト間で共有される
これが一番厄介でした。
httpClient は、同一リクエスト内で動くすべてのサーバスクリプトが同じインスタンスを共有します。
あるスクリプトが設定を変えると、次に走るスクリプトはその状態を引き継ぎます。
実際にやらかしたのがこれです。検証用スクリプトが
httpClient.Encoding = 'iso-8859-1';
としたまま終了したところ、次に走った埋め込み処理が日本語を iso-8859-1 で送信し、
文字化けしたテキストのベクトルが返ってきました。
エラーにはなりません。 HTTP 200 でベクトルも返ってきます。
ただ類似度の計算結果だけがおかしくなり、正解のはずのレコードが上位に来なくなります。
(正常時)類似度 0.921 複合機で印刷すると紙詰まりエラーになる
(壊れた時)類似度 0.778 有給休暇の残日数はどこで確認できますか
「AI の精度がいまいちだな」と思ってプロンプトを何度も直してしまいました。
対策は、使う直前に毎回すべて設定し直すことです。
httpClient.RequestUri = ENDPOINT;
httpClient.RequestHeaders.Clear(); // ヘッダも残っている
httpClient.MediaType = 'application/json';
httpClient.Encoding = 'utf-8'; // 明示的に戻す
httpClient.TimeOut = 45000;
httpClient.Content = body;
RequestHeaders も同様です。前のスクリプトが付けた Authorization が残っていると、
別サービスに他所のトークンを送ることになりかねません。Clear() は必ず呼んでください。
3. httpClient は multipart/form-data を送れない
httpClient.Content は文字列で、内部的には StringContent として送信されます。
MediaType に boundary 付きの値を入れることはできますが、送信時に落ちます。
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 経由でやりとりする
4. items.Get() の戻り値は .length ではなく .Length
items.Get(siteId) が返すのは .NET の配列です。JavaScript の配列ではありません。
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 になり、
何事も無かったかのように処理が終わります。 「0 件しか見つからない」ときはここを疑ってください。
同じ理由で rows.forEach(...) や rows.map(...) も使えません。
5. columns.X.ChoiceHash は読めない
分類項目の選択肢をスクリプトから取りたくなりますが、ChoiceHash は常に null です。
typeof columns = object
columns.ClassA = ok
LabelText = 種別 ← これは読める
ChoiceHash = null ← これは読めない
ChoiceHash はサーバスクリプトから選択肢を設定するためのプロパティで、
現在の選択肢を取得する用途では機能しません。
選択肢一覧が必要なら、スクリプト側に定義を持つか、拡張SQL で
Sites.SiteSettings(JSON)を読んでパースすることになります。
6. 拡張SQLは "Api": true が無いと NullReference で落ちる
extendedSql.ExecuteRow('MySql', ...) を呼ぶと、いきなりこれが出ます。
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 が無いだけです。
{
"Name": "GetLatestImage",
"Api": true, ← これが無いと呼べない
"SiteIdList": [5],
"CommandText": "select ..."
}
サーバスクリプトから呼べるのは Api が true の拡張SQLだけで、
条件に合わない場合は「見つからない」ではなく null が返り、その先で落ちます。
名前の綴り間違いでも同じ例外になります。
7. AfterUpdate では saved が更新後の値になっている
「値が変わったときだけ処理する」を書こうとして、必ずここで止まります。
// AfterUpdate での実測
saved.Status = 300
model.Status = 300 ← 更新前の値が取れない
saved は「保存済みの値」なので、AfterUpdate の時点では当然そうなります。
BeforeUpdate なら saved に更新前の値が入っています。
// BeforeUpdate での実測
saved.Status = 300
model.Status = 900 ← 差分が取れる
とはいえ BeforeUpdate で外部通知まで済ませると、保存が失敗しても通知だけ飛びます。
回避策: context.UserData で橋渡しする
context.UserData は 同一リクエスト内のサーバスクリプト間で共有される入れ物です。
BeforeUpdate で書いて AfterUpdate で読めます。
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) {
// 保存が完了してから通知する
}
「差分を見て、保存後に何かする」の定番パターンとして覚えておくと便利です。
8. 通知の HttpClient 型はサーバスクリプトから使えない
notifications で汎用 Webhook(Type = 9)を送ろうとすると二段階でつまずきます。
まず既定で無効です。
{
"HttpClient": false,
…
}
true にして再挑戦すると、今度は例外になります。
ERR Error: Value cannot be null. (Parameter 'name')
HttpClient 型の送信処理は Encoding / MediaType / MethodType / Headers を参照しますが、
サーバスクリプト側のラッパーはこれらのプロパティを持っておらず、
Send() が組み立てる通知にも引き継がれません。結果 Encoding が null のまま
Encoding.GetEncoding(null) が呼ばれて落ちます。
Slack 型(Type = 2)や Teams 型(Type = 6)はこれらを使わないので問題なく動きます。
Slack 型は Address に {"text": "..."} を POST するだけなので、
受け口が Slack でなくても、その形式で受け取れる Webhook なら流用できます。
var n = notifications.New();
n.Type = 2; // Slack
n.Address = WEBHOOK_URL;
n.Title = 'タイトル';
n.Body = '本文';
n.Send();
9. notifications の Send() は常に true を返す
前項に関連します。Send() の戻り値は送信結果ではありません。
- 通知タイプが無効化されていても
true - パラメータで機能が閉じられていても
true
戻り値で成否を判定しないでください。 送れているかどうかは受け口側で確認するしかありません。
開発中は受信を確認できる Webhook を用意しておくのが確実です。
10. サーバスクリプトのエラーはSysLogsにしか出ない
サーバスクリプトが落ちると、画面にはこれだけ出ます。
サーバスクリプトの実行に失敗しました。
サーバスクリプトを確認・修正してから、再度お試しください。
コンテナのログ(docker logs)にも出ません。スタックトレースは SysLogs テーブルにあります。
select "SysLogId", "ErrMessage", "ErrStackTrace"
from "SysLogs"
where "ErrMessage" is not null
order by "SysLogId" desc
limit 3;
前述の「拡張SQL の Api 忘れ」も、これを見て初めて原因が分かりました。
サーバスクリプトを書くときは、この SQL をすぐ叩けるようにしておくと調査が段違いに速くなります。
おまけ: 添付ファイルのバイナリを取り出す
サーバスクリプトには添付ファイルの中身を読む API がありません。
_file_cs は ReadAllText しか持たず、しかも既定で無効です。
実体は Binaries テーブルにあるので、拡張SQL 経由で取り出します。
select "FileName", "ContentType", "Size", encode("Bin", 'base64') as "Base64"
from "Binaries"
where "ReferenceId" = @ReferenceId
and "BinaryType" = 'Attachments'
order by "BinaryId" desc
limit 1
BinaryType の値に注意してください。
| 添付のしかた | BinaryType |
|---|---|
| 添付ファイル項目にドロップ | Attachments |
| 長文項目(Markdown)に画像を貼り付け | Images |
「添付したのに取れない」ときは、たいてい Images で入っています。
BinaryStorage.json の Provider が Rds(既定)のときの話です。
ファイルシステムや Azure Blob に保存している環境では Bin が空になります。
まとめ
| # | 落とし穴 | ひとことで |
|---|---|---|
| 1 | スクリプトのタイムアウト | 既定 10 秒。Script.json で延ばす |
| 2 |
httpClient の状態共有 |
使う直前に毎回全部設定し直す |
| 3 | multipart 非対応 | 送れない。JSON で送れる API を選ぶ |
| 4 |
items.Get() の戻り値 |
.length ではなく .Length
|
| 5 | ChoiceHash |
読めない(設定専用) |
| 6 | 拡張SQL |
"Api": true 必須。無いと NullReference |
| 7 |
AfterUpdate の saved
|
更新後の値。context.UserData で橋渡し |
| 8 | 通知の HttpClient 型 |
使えない。Slack / Teams 型を使う |
| 9 |
Send() の戻り値 |
常に true。あてにしない |
| 10 | エラーの調べ方 |
SysLogs テーブルを見る |
共通しているのは、「エラーにならないまま静かに間違う」ものが多いという点です。
length が undefined でループが回らない、Encoding が残っていて文字化けする、
Send() が true を返すのに届いていない——どれも動いているように見えます。
サーバスクリプトから外部 API を叩くときは、まず SysLogs を見る癖と、
受信を目視できる相手(ローカルの Webhook 受け口など)を用意しておくことをおすすめします。
これだけで調査時間がかなり変わります。