GASからDifyを呼び出してちょっと重めのLLMワークフローの処理をさせていたときに504エラー。
あ、解決したのもブログを書いたのも、もちろんAIですよ
直面した問題:亡霊のような「504 Gateway Timeout」
Dify(セルフホスト版)を構築し、GASから UrlFetchApp でワークフローを呼び出していました。 短い処理は問題ないのですが、LLMが思考を重ねる長い処理になると 504エラー が発生します。
- リクエスト設定:
response_mode: 'blocking'(完了まで待つモード) - 症状: 約60秒〜で Nginx/ロードバランサーが切断する
Dify自体は裏で動いているのに、間の通信経路(リバースプロキシ等)が「応答がない(沈黙している)」と判断して接続を切ってしまうのです。
2. なぜ「ポーリング」で解決できないのか?
通常、こういう場合は「非同期実行」が定石です。
- キックして
task_idだけもらう - 定期的にステータスを確認しに行く(ポーリング)
しかし、ここには Dify × 504エラー特有の罠 がありました。
- 罠: 504エラーが発生すると、返ってくるのはJSONではなくHTMLのエラーページ。
- 結果: ワークフローの
run_idやtask_idが取得できない。
つまり、「IDが取れないから、問い合わせようがない」という詰み状態に陥ります。
3. 解決策:「擬似ストリーミング」という発想
そこで採用したのが、「GASでストリーミング(SSE)を受け取る」というアプローチです。
ただし、 GASの UrlFetchApp ってSSEのストリーミング(逐次処理)非対応です。GASはリアルタイムにチャンクを処理することはできません。しかし、ストリーム完了後に全データを一括取得で「受信自体はできる」のです。
仕組み:生存報告としてのストリーミング
blocking モードを streaming モードに変えることで、通信の挙動がこう変わります。
- Blocking: 処理完了まで「無言」 → 経路がタイムアウト判定(504)
- Streaming: Difyが処理中も断続的にデータ(
data: ...)が流れてくる → 経路が「生きてる!」と判断して接続維持→ 504回避
GAS側はストリームをリアルタイム処理できませんが、「通信が完了するまで待ち、溜まった全データを文字列として一括取得する」ことは可能です。

4. 実装コード (TypeScript/GAS)
修正は非常にシンプルです。
変更点1:リクエスト時に streaming を指定する
function runDifyWorkflowStreaming() {
const url = 'YOUR_DIFY_URL/workflows/run';
const payload = {
"inputs": { ... },
"response_mode": "streaming", // ここ変えるだけ
"user": "gas-user"
};
const options = {
'method': 'post',
'contentType': 'application/json',
'headers': { 'Authorization': 'Bearer ...' },
'payload': JSON.stringify(payload),
'muteHttpExceptions': true // エラーハンドリングのため必須
};
// GASはストリーム終了まで待ち、全データを1つの巨大な文字列として受け取る
const response = UrlFetchApp.fetch(url, options);
// SSE形式のログを一括パース
const result = parseSSEForWorkflowResult(response.getContentText());
console.log(result);
}
変更点2:SSEログから結果を抽出するパーサー
GASが受け取るデータは data: {...}\n\n data: {...} という文字列の塊です。ここから最終結果(workflow_finished)だけを抜き出します。
/**
* SSE形式の文字列から workflow_finished イベントのデータだけを抽出する
*/
function parseSSEForWorkflowResult(sseString: string): any {
const lines = sseString.split('\n');
for (const line of lines) {
// SSEの行は "data: " で始まる
if (line.startsWith('data: ')) {
try {
const jsonStr = line.substring(6); // "data: " を除去
const data = JSON.parse(jsonStr);
// 完了イベントを探す
if (data.event === 'workflow_finished') {
// data.data に outputs や status が含まれる
return data.data;
}
} catch (e) {
// pingイベントやパースエラーは無視して次へ
continue;
}
}
}
return null;
}
5. まとめと制約
この手法により、ロードバランサーやNginxの設定を変更することなく、コード側の変更だけで504エラーを回避できました。
- メリット: サーバー設定不要、Difyの戻り値(Output)もしっかり受け取れる。
- デメリット: GAS自体の「6分の壁(最大実行時間)」は超えられない。
- ※6分を超える超長時間処理の場合は、この方法でもタイムアウトするため、Cloud Functionsなどを噛ませる必要があります。