GAS × Dify で504タイムアウトを回避する”擬似ストリーミング”

GASからDifyを呼び出してちょっと重めのLLMワークフローの処理をさせていたときに504エラー。
あ、解決したのもブログを書いたのも、もちろんAIですよ

直面した問題:亡霊のような「504 Gateway Timeout」

Dify(セルフホスト版)を構築し、GASから UrlFetchApp でワークフローを呼び出していました。 短い処理は問題ないのですが、LLMが思考を重ねる長い処理になると 504エラー が発生します。

  • リクエスト設定: response_mode: 'blocking' (完了まで待つモード)
  • 症状: 約60秒〜で Nginx/ロードバランサーが切断する

Dify自体は裏で動いているのに、間の通信経路(リバースプロキシ等)が「応答がない(沈黙している)」と判断して接続を切ってしまうのです。

2. なぜ「ポーリング」で解決できないのか?

通常、こういう場合は「非同期実行」が定石です。

  1. キックして task_id だけもらう
  2. 定期的にステータスを確認しに行く(ポーリング)

しかし、ここには Dify × 504エラー特有の罠 がありました。

  • 罠: 504エラーが発生すると、返ってくるのはJSONではなくHTMLのエラーページ
  • 結果: ワークフローの run_idtask_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などを噛ませる必要があります。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次