簡潔に言うと: 画像 API のレスポンスは通常 10 MB から数十 MB に及び、ほとんどの場合、問題は レスポンスのダウンロード中 に発生します(リクエスト本文が大きすぎる からではありません。プレーンな text-to-image でも同様に失敗します)。まずコンソールログがどのカテゴリに当たるかを確認し、その後で下の macOS / Linux / Node.js のセルフチェックを実行してください。
エラーの見え方
同じ根本原因でも、どちら側を見るかによって、まったく異なる2つの顔で現れます。ゲートウェイ側から
クライアント側から
まず方向を見極めます: 誰が切断したか
write_response_body_failed コードが最大の手がかりです。これは、ゲートウェイがレスポンス本文を呼び出し元へ書き戻す途中で失敗したことを意味します。つまり、上流のモデルエラーではなく、下流側の問題です。結果自体はすでに生成されており、あなたに送ろうとしている途中で接続が切れました。
これはリクエスト本文が大きすぎることが原因ではありません。 画像編集では参照画像をアップロードするため、アップロードサイズが疑われがちですが、リクエスト本文が数百バイトしかない単純な text-to-image でも、同じくらい頻繁に壊れます。壊れているのはレスポンスのダウンロードのほうです。画像ペイロードは 10MB から数十MB に及び、全体の経路の中で最も壊れやすい区間です。
下流側の切断
write_response_body_failed, connection reset by peer, クライアント側の SSL EOF。
ゲートウェイが本文を送っている最中に接続が消えました。プラットフォームがこれに対して 500 を返す場合、課金されません。詳しくは下の課金セクションを参照してください。上流側の失敗(チャネル側)
上流側のタイムアウト、
upstream_error、上流ペイロードを伴う 5xx、または異常な finishReason を伴う HTTP 200。
これらは本当のチャネル側の問題です。x-request-id をサポートに渡してください。この呼び出しについて、コンソールログが示す最も強い手がかり
何か触る前に、まずコンソールの呼び出しログを確認してください。コストはかからず、クライアント側のどの手順よりも早く候補を絞れます。切り分けの4手順
順番に確認してください。ほとんどは最初の2つで解決します。1
アプリケーション層での読み違いを除外してください: 受け取っていたのに保持できなかった可能性があります
「画像を受け取れなかった」は、コードが例外を投げて「request failed」として捕捉された後に下された結論であることがほとんどです。つまり、バイトが届いていなかった証拠ではありません。最も典型的なのは、シリーズごとのフィールドとプレフィックスの違いは、base64 プレフィックス参照にあります。
gpt-image-2-all が既定で b64_json を返すのに data: プレフィックスがないため、data[0].url を読むコードが undefined を受け取り、下流で例外を投げ、失敗と判定され、再試行が走るケースです。その結果、再び課金されます。症状はネットワーク障害とまったく同じですが、ネットワーク障害である必要はありません。確認のために1行出力してください:2
すべてのチャネルとモデルが同時に失敗しているか確認してください
2つの異なるチャネルで2つの異なるモデルを動かしていて、同じ時間帯に同じエラーが出ているなら、チャネル固有の原因は実質的に除外できます。上流側がそんなにきれいに同時に失敗することはありません。
3
クライアントのランタイムを確認してください: Python の TLS スタック、または Node.js の undici タイムアウトです
Python: TLS スタックのバージョンを確認してください。Node.js: undici の 3 つのタイムアウトを確認してください。どちらも次の2つのセクションで扱います。実際にはこれが最も多い根本原因で、原因はすべて手元の環境にあります。1つのコマンドで確認できます。
4
同時実行数を下げ、逐次実行にして、ローカルプロキシを無効にしてください
同じリクエストを同時実行数 1-2 で、VPN またはプロキシを無効にして再実行してください。その方法では再現しないなら、問題はクライアントの接続処理、ローカルリソース(コネクションプール、ファイルディスクリプタ、メモリ)、またはネットワーク経路にあります。チャネル側ではありません。
最有力の容疑者: クライアントの TLS スタック(特に macOS)
macOS に同梱される Python(/usr/bin/python3)は、OpenSSL ではなく LibreSSL 2.8.3 にリンクしています。 これに urllib3 v2 を組み合わせると、大きなレスポンスボディの同時ダウンロード中に SSLEOFError が高い確率で発生します。クライアントが一方的に切断し、ゲートウェイは当然ながら connection reset by peer の大量発生を記録します。
確認するための1コマンド
requests を import したときにこの警告が出るなら、同じ兆候です:
修正: インタープリタを切り替える
urllib3 をダウングレードしないでください — 代わりに、適切な OpenSSL でビルドされた Python を使ってください:計測比較(2026-07-29, UTC+8)
Nano Banana シリーズ(gemini-3-pro-image / gemini-3.1-flash-image)での2チャネル比較テストから得た実測値です:
結論は明白です: インタープリタを切り替えるだけで桁違いの差が出ており、その前に両チャネルが同時に失敗していた事実だけでも、チャネル起因ではないことはすでに証明されていました。
Linux サーバーで確認すべきこと(まったく別のリスト)
上の TLS スタックのチェックは、実質的にどの Linux ボックスでも通ります。ディストリビューションの Python は通常の OpenSSL にリンクしているため、LibreSSL の罠はそこにはありません。そのチェックだけで止めないでください。 サーバー側の問題は egress 経路 と コンテナの制限 にあり、ローカル開発とはまったく別世界です。
1. クラウド NAT ゲートウェイとロードバランサーのアイドルタイムアウト(サーバー側で最も多い原因)
これは本番環境でのconnection reset by peer の最大の原因です。たとえば AWS NAT Gateway は、固定で変更不可の 350 秒のアイドルタイムアウト を強制し、発火すると FIN ではなく RST を送ります。つまりクライアントからはまさに ECONNRESET に見えます。
厄介なのは、これが 連鎖する ことです。プールされた接続が 350 秒を超えてアイドル状態になると、それらはすべて死んでいるため、リクエストは最初の 1 本で RST を受け、クライアントは透過的に次のプール接続へ再試行します。しかしその接続も同じくアイドルで、やはり RST を受けます。 症状は「しばらく静かだったのに、その後いくつかの呼び出しが立て続けに失敗し、また何事もなかったように戻る」です。
対処法(どれでも可。最初の 2 つを推奨します):
- TCP keepalive を 350 秒未満に設定 して、静かな時間帯でも通信が流れるようにする;
- 接続がプール内でアイドルのままでいられる時間を制限 して、死んでいる可能性のあるものを破棄する(Node:
new Agent({ keepAliveTimeout: 60_000 }); Pythonrequests:HTTPAdapterでプールを設定する); - NAT ゲートウェイを完全に迂回する。たとえば VPC endpoints を使う。
2. TCP keepalive のデフォルトは実質的に「オフ」です
Linux ではデフォルトでtcp_keepalive_time が 7200 秒(2 時間) に設定されており、上記のどのアイドルタイムアウトよりもはるかに長いため、実運用では役に立ちません:
3. コンテナネットワークの MTU
Docker / Kubernetes のオーバーレイネットワーク(flannel VXLAN など)は、一般に MTU を 1500 ではなく 1450 にしています。これに経路上の PMTUD ブラックホールが組み合わさると、典型的な「小さいリクエストは常に問題なし、大きいレスポンスは常に詰まる」が発生します:4. コンテナのメモリ制限 → プロセスが OOMKilled される
4K の base64 ペイロード 1 つで 20〜30MB に達することがあります。これをresp.json() で丸ごと読み込み、さらに同時実行数があるとコンテナのメモリ制限を簡単に超え、カーネルがプロセスを kill します。これもまた 「接続が切れただけ」に見えます:
5. プロキシ環境変数(サーバーではいちばん気づきにくいもの)
サーバーには、/etc/environment、systemd ユニット、Dockerfile に設定されたグローバルな HTTP_PROXY / HTTPS_PROXY / NO_PROXY の値が残っていることがよくあります。設定した本人ですら長い間忘れていることが多いです。さらに厄介なのは、言語ごとにそれらを尊重するかどうかが一致しないことです。
この不一致は本当に紛らわしい結果を生みます。あるマシンでは curl と Python はプロキシを通るのに、Node は直接接続する(またはその逆)ため、両者の結果が食い違い、トラブルシューティングでも矛盾した結論になります。まず確認してください:
api.apiyi.com を NO_PROXY に設定します。あるいは、そもそもプロキシ変数を使わないようにします。
サーバー側の一発セルフチェック
Node.js: SDK の timeout では届かない 3 つの独立したタイムアウト
Node 18+ の組み込み fetch は undici 上で動作し、リクエストの 3 つの段階をカバーする 3 つの独立したタイムアウト があります。「でもタイムアウトを 5 分に設定したのに」という場合、たいていはそのどれでもない 4 つ目の値を変更しただけです:
正しい設定
undici の 3 つのタイムアウトを広げるには、グローバルまたはリクエストごとにAgent が必要です:
maxRetries はデフォルトで 2 で、接続エラーを自動再試行します
openai-node は デフォルトで maxRetries: 2 になっており、接続エラーとタイムアウトの両方がその自動再試行の対象です。そのため、1 回の論理的な呼び出しで、コードに再試行ロジックが一切なくても 実際のリクエストが 3 回 発生しえます(それぞれが課金対象かどうかは、「課金への影響」カテゴリのどれに属するかで決まります)。
画像エンドポイントは高コストな同期長時間リクエストなので、maxRetries: 0 を必ず明示的に設定し、再試行ロジックは自分で管理してください。独自のバックオフと試行上限を使ってください。課金ルールは 再試行戦略 にあります。
すでに切断済みの接続を keep-alive で再利用する場合
undici はデフォルトで keep-alive によるコネクションプーリングを有効にします。VPN、NAT、またはプロキシがアイドル状態の接続を黙って回収しても、クライアントはそれに気づかず、次のリクエストでもその接続をプールから取り出します — 書き込みは即座に RST を受け取り、read ECONNRESET として表面化します。
呼び出しの間隔が空くとき、これは ECONNRESET の最も一般的な原因です。また、「エラーが 1 つの時間帯に集中する」ことと「最初の再試行ですら失敗する」ことの両方を説明します。再利用を無効にして確認してください:
ローカルプロキシ / VPN: 画像エンドポイントが最初に露出するホップ
APIYI は中国本土内から直接到達でき、プロキシや VPN は不要です(API を使うのにプロキシは必要ですか? を参照)。そのため、プロキシをオフにして再テストすることが、試せる中で最も安く、情報量の多い単独ステップになります。ただし、はっきりさせておくと、プロキシはあくまで最も疑わしい変数であり、確定した根本原因ではありません。下のマトリクスが、実際に障害箇所を特定します。
fake-ip / ルーティングルールの見落とし
プロキシの fake-ip モードでは、ルールにマッチしないと
198.18.x.x のような到達不能アドレスに振り分けられ、ちょうど 10 秒の接続タイムアウトが発生します。これは「接続が遅い」わけではなく、そもそも経路がないので、connect.timeout を上げても助かりません。実際に到達した remote_ip を必ず記録してください。生成中にアイドル接続として回収される
リクエスト送信後 30〜60 秒のあいだバイトが 1 つも流れず、プロキシがアイドルポリシーに従って接続を回収します。特徴は、画像サイズに関係なく 失敗時刻がきりのいい数字に収まる ことです。30 / 60 / 120 秒などです。
MTU / PMTUD のブラックホール
トンネル MTU が経路 MTU を下回っている一方で、ICMP の「fragmentation needed」が破棄されると、PMTUD が壊れます。典型的な特徴は、小さいリクエストは常に正常で、大きいレスポンスは常に詰まる ことです。受信バイト数は数 KB から数十 KB で止まります。トンネル MTU を 1400 前後まで下げると、たいてい解決します。
MITM 復号とフルバッファリング
HTTPS 復号を有効にしたプロキシは、大きなボディを丸ごとバッファリングしてサイズ上限に達することがよくあります。あるいは、chunked を
Content-Length に書き換えて長さを誤り、RST を発生させます。これも MB 規模の画像レスポンスにだけ起き、テキスト呼び出しには起きません。診断マトリクス
ここがこの節の核心です。軸は 2 つだけです。失敗が最初のバイトより前に起きたか後に起きたか、そして何バイト到着したかです。
最後の行は最も価値の高い単独テストです。ボディを数 MB から約 1KB まで縮められます。URL モードが一貫して成功し、base64 モードが一貫して失敗するなら、問題は転送量に比例しているため、最初の 2 列はこの時点で除外できます。
完全なタイミングプロファイル用の 1 コマンド
connect の値がなければ → 接続段階。ttfb がなく、total がきりのいい数字なら → アイドル回収。bytes が完全で、total ≈ ttfb + 300 に加えて curl: (18) があるなら → 終端子欠落。bytes が数十 KB で止まるなら → MTU。
その他の一般的なトリガー
実行中の手動中断
デバッグ中の Ctrl+C、プロセスの再起動、ホットリロード、実行中スクリプトの強制終了 — 送信途中の大きなレスポンスはどれも、ゲートウェイに
write_response_body_failed を残します。これは、最もよく「チャネルが不安定だ」と誤解される誤警報です。外側のタイムアウトが先に発火する
タスクキューのワーカーのタイムアウト、Serverless の実行制限、ゲートウェイ/CDN のオリジンタイムアウト(通常はデフォルトで 60 秒)です。生成時間より短いどのレイヤーでも、先に接続を切断します — 必読事項とベストプラクティス を参照してください。
コネクションプールと過剰な同時実行数
コネクションプールの上限、ローカルのファイルディスクリプタ制限、NAT/ファイアウォールが長時間接続を静かに回収してしまうこと。大きなレスポンスははるかに長く接続が開いたままになるため、テキストエンドポイントよりもこれらの制限にずっと頻繁に達します。
レスポンスボディがメモリを使い切る
1 つの 4K base64 ペイロードだけで 20-30MB に達することがあります。
resp.json() を同時実行数下で一度にすべて読み込むと、コンテナメモリを使い切ってプロセスが OOM-killed され、これもまた「理由もなく接続が切れた」ように見えます。課金への影響: どのケースが課金され、どのケースが課金されないか
この 2 つの状況は常に混同されますが、課金のされ方は正反対です:プラットフォームが 500 write_response_body_failed を返した場合 — 課金されません
このエラーは、ゲートウェイが画像データをあなたに書き戻している最中に接続が切れたことを意味します。プラットフォームは内部で自動的に 2〜3 回再試行しますが、すべて失敗した場合にのみ 500 を返します。このケースでは課金は発生しません。 同じリクエストでこのエラーが何度続いても、請求書に対応する課金は残りません — この失敗に対して料金を支払うことはありません。write_response_body_failed が記録されたかを確認してください。
したがって、再試行ポリシーもそれに合わせて節度を保つべきです: トランスポートレベルの失敗は再試行する価値がありますが、再試行のたびに別個に課金される呼び出しになる可能性があります(どちらのカテゴリに当たるかによります)。無制限の再試行ループは決して書かないでください。
正しく再試行する方法
基本ルールは、トランスポート層の例外だけを再試行し、HTTPレベルのエラーは絶対に再試行しないことです。4xx を 1万回再送しても、4xx のままであり、時間の無駄です。レスポンスは全体を読み込むのではなくストリーミングしてください
大きなボディでは、stream=Trueを使ってチャンクごとに読み取ってください。ピークメモリを抑えられ、転送のどの段階で失敗したのかを正確に把握できます。
本当にサポートに問い合わせる価値がある場合
自己切り分けが済んだら、以下のいずれかに該当する場合はエスカレーションしてください。- 正しい OpenSSL インタープリタに切り替え、実行をシリアルにしても、まだ安定して再現する;
- 同じ時間帯に他は正常なのに、特定の 1 つのチャネルまたはモデルだけ が失敗する;
- レスポンスボディが 完全に到着している(バイト数が
Content-Lengthと一致する)のに、接続がタイムアウトするまで閉じない — これは上流で chunked の終端が欠けていることを意味し、チャネル側の問題です; - エラーが明確に上流向け(
upstream_error、生の上流 5xx)である。
x-request-id、呼び出し時刻(タイムゾーン付き、例: 2026-07-29 14:32 (UTC+8))、モデル名、imageSize のような主要パラメータ、生のクライアント例外、そしてすでに完了した自己確認手順を含めてください。
関連ドキュメント
必読事項とベストプラクティス
同期呼び出し、モデルごとのタイムアウト、base64 の処理、切断時の課金
独自の非同期キューを構築する
同期呼び出しをタスクキューでラップし、リトライで時折のドロップを吸収する
プロキシは必要ですか?
APIYI はプロキシなしで直接接続します。証明書と DNS の問題をセルフチェックします
Gemini 画像エラー処理
Gemini 画像生成におけるエラーコードと finishReason の処理