メインコンテンツへスキップ

課金優先: write_response_body_failed の 500 は 課金されません

ゲートウェイが 500 を write_response_body_failed / connection reset by peer で返す場合、プラットフォーム側では すでに内部で 2〜3 回再試行済み であり、すべて失敗したあとにのみエラーを表示します。これらのリクエストには一切課金されません。そのため、ログがこれらのエラーで埋まっていても、請求書に対応する課金は表示されません。失敗に対して料金を支払っているわけではありません。別のケースは 課金されます(クライアントが早々に切断した場合)ので、下の「課金への影響」セクションを参照してください。
簡潔に言うと: 画像 API のレスポンスは通常 10 MB から数十 MB に及び、ほとんどの場合、問題は レスポンスのダウンロード中 に発生します(リクエスト本文が大きすぎる からではありません。プレーンな text-to-image でも同様に失敗します)。まずコンソールログがどのカテゴリに当たるかを確認し、その後で下の macOS / Linux / Node.js のセルフチェックを実行してください。

エラーの現れ方

同じ根本原因であっても、どちら側から確認するかによって、まったく異なる2つの現れ方をします。

ゲートウェイ側

クライアント側

Node.js では「killed」と「closed」を区別する: ECONNRESET は TCP RST が届いたことを意味します — 接続が通信経路上の何らかの要素によって強制終了(killed)されたことを表し、ネットワークホップの問題を指し示しています。SocketError: other side closed / ERR_STREAM_PREMATURE_CLOSE は対向側が正常に切断(gracefully)(FIN)したことを意味し、チャンク終端子の欠落など、サーバー側の終了処理の問題を示唆しています。この2つはまったく異なる原因を指し示しているため、同一の事象として扱わないでください。また、UND_ERR_* は undici(Node 18+ の組み込み fetch の基盤エンジン)からのみ出力されます。一方、read ECONNRESET は libuv のトップレベルの文言であり、axios / node-fetch / http モジュールのいずれからも出力されます。両方の種類が同時に発生している場合は、まずアプリケーションに2つの異なる HTTP パスが存在しないか確認してください — その場合、それらは決して同一のインシデントではありません。

まず方向を見極めます: 誰が切断したか

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 をサポートに渡してください。

この呼び出しについて、コンソールログが示す最も強い手がかり

何か触る前に、まずコンソールの呼び出しログを確認してください。コストはかからず、クライアント側のどの手順よりも早く候補を絞れます。
ここから導ける結論は、よく逆に理解されます。UND_ERR_CONNECT_TIMEOUT のような接続段階の失敗では課金は発生しません。リクエストがゲートウェイに届いていないからです。したがって、「接続タイムアウトが大量にある」のに「課金も大量にある」のであれば、それらは同じリクエストではありません。1つの根本原因で全部を説明しようとせず、別々に調べてください。

切り分けの4手順

順番に確認してください。ほとんどは最初の2つで解決します。
1

アプリケーション層での読み違いを除外してください: 受け取っていたのに保持できなかった可能性があります

「画像を受け取れなかった」は、コードが例外を投げて「request failed」として捕捉された後に下された結論であることがほとんどです。つまり、バイトが届いていなかった証拠ではありません。最も典型的なのは、gpt-image-2-all が既定で b64_json を返すのに data: プレフィックスがないため、data[0].url を読むコードが undefined を受け取り、下流で例外を投げ、失敗と判定され、再試行が走るケースです。その結果、再び課金されます。症状はネットワーク障害とまったく同じですが、ネットワーク障害である必要はありません。確認のために1行出力してください:
シリーズごとのフィールドとプレフィックスの違いは、base64 プレフィックス参照にあります。
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 を受けます。 症状は「しばらく静かだったのに、その後いくつかの呼び出しが立て続けに失敗し、また何事もなかったように戻る」です。
これは Node.js セクションの「keep-alive が死んだ接続を再利用する」ケースと同じ仕組みです。サーバーでは、原因はたいていローカルのプロキシソフトではなく、クラウド事業者の NAT ゲートウェイ です。
対処法(どれでも可。最初の 2 つを推奨します):
  • TCP keepalive を 350 秒未満に設定 して、静かな時間帯でも通信が流れるようにする;
  • 接続がプール内でアイドルのままでいられる時間を制限 して、死んでいる可能性のあるものを破棄する(Node: new Agent({ keepAliveTimeout: 60_000 }); Python requests: HTTPAdapter でプールを設定する);
  • NAT ゲートウェイを完全に迂回する。たとえば VPC endpoints を使う。
他のクラウドや自前運用のロードバランサーでは別のアイドル値を使いますが、アプローチは同じです。経路上で最も短いアイドルタイムアウトを見つけ、その値より下に keepalive を設定してください。

2. TCP keepalive のデフォルトは実質的に「オフ」です

Linux ではデフォルトで tcp_keepalive_time が 7200 秒(2 時間) に設定されており、上記のどのアイドルタイムアウトよりもはるかに長いため、実運用では役に立ちません:
HTTP クライアント自体で keepalive を有効にする方がより堅牢です。コンテナではカーネルパラメータを変更できないことが多いためです。

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 は直接接続する(またはその逆)ため、両者の結果が食い違い、トラブルシューティングでも矛盾した結論になります。まず確認してください:
APIYI は直接到達可能なので、サーバーでは通常 api.apiyi.com を NO_PROXY に設定します。あるいは、そもそもプロキシ変数を使わないようにします。

サーバー側の一発セルフチェック

回線上のパケットロス:ヘッダーは届くが、ボディが途中で切断される

お使いのサーバーがAPIYIを直接呼び出し、そのマシン(またはそのデータセンター)のアウトバウンド回線の品質が低い場合、特徴的な障害が発生します。HTTP 200ヘッダーは届き、ボディのごく一部がダウンロードされた後、接続が切断されるという現象です。画像の生成自体は正常に完了しています。切断は、結果をお使いのサーバーへ送り返すパブリックネットワークの経路上で発生しています。

代表的なエラー

見分ける方法:3つの兆候

  1. ステータスは200で、エラーメッセージに残りの不足バイト数が表示されている。 クライアントが残りのバイト数を報告できる場合、レスポンスにはContent-Lengthが含まれていました。これは、ゲートウェイが送信を開始する前に完全な結果を保持しており、その合計サイズを把握していたことを意味します。生成は成功しており、途切れたのはダウンロード中のみです。
  2. 合計時間が生成時間よりもはるかに長い。 curlのtime_starttransfer(最初の1バイトを受信するまでの時間)はおおよそ生成時間と等しくなります。time_totalが数分以上長く、size_downloadがわずか数十〜数百KBである場合、データが正常に届いていません。
  3. 特定の時間帯に発生し、別のマシンでは問題がない。 同じサーバーであっても、1日のうち特定の時間帯には頻繁に失敗し、それ以外の時間帯には正常に動作します。まったく同じタイミングで、別のマシン(またはお使いのノートPC)から同一のリクエストを実行すると正常に完了します。
永久にハングし続けずに「connection closed」で終了する理由:パケットロスが深刻な回線では、データパケットと確認応答の両方が失われます。**サーバーがクライアントからの確認応答を長時間受信できない場合、接続を切断します。**そのためタイムアウトのように見えますが、根本的な原因はパケットロスです。ご自身でタイムアウト時間を延ばしても解決しません。

セルフチェックの手順

1

curlで再現し、タイミングプロファイルを記録する

結果の見方:http=200、sizeが明らかにContent-Lengthよりも小さく、totalがttfbを大幅に上回っている場合は、ダウンロードに問題があることを意味します。次のステップに進んでください。
2

pingとmtrでパケットロスを測定する

正常な回線ではパケットロスは0%近くになります。最終ホップまで引き継がれているロスのみをカウントしてください。後続のホップに問題がないにもかかわらず途中の1つのホップだけでロスが発生している場合、通常はそのルーターがpingの応答の優先順位を下げているだけです。障害が発生する時間帯に実行してください。テスト時に回線状態が良好であれば何も検出されません。
3

別のマシンと比較する

同時に、別のサーバー(可能であれば別のデータセンターやクラウドプロバイダー)またはノートパソコンでステップ1のコマンドを実行してください。比較用のマシンでは正常に動作し、このマシンだけで失敗する場合は、APIではなく、このマシンのネットワーク下り(egress)に問題があります。
4

時間帯ごとのパターンを確認する

すべての呼び出しの時間、http_code、size、totalを記録し、特定の時間帯に障害が集中していないか確認してください。特定の時間帯に発生する障害は、回線混雑の典型的な兆候です。
実際の事例(2026-09-27):画像は48秒で生成され、完全なレスポンスはすでにegressノードに到達していました。しかし、当時お客様のデータセンターの回線では17%〜37%のパケットロスが発生しており(同時刻において、同一ノードから中国国内の他のサーバーへのロスは0%でした)、お客様は約4%のデータしか受信できませんでした。同じ時間帯に同一ノードを利用していた他のお客様は全員、レスポンスを完全に受信できていました。

緩和策

  1. レスポンスを小さくする(即効性あり): gpt-image シリーズの場合は、output_format=jpeg と output_compression=85 を追加します。2026-09-27 の測定では、n=1 のレスポンスは約 2.4 MB から約 0.5 MB に減少し、n=5 では約 12 MB から約 2.6 MB に減少して、約 4.7 倍縮小しました。送信するデータ量が少ないほど、パケットロスのある回線を通過できる可能性が高くなります。response_format: "url" をサポートするモデル(gpt-image-2-vip や gpt-image-2-all など)は代わりにリンクを返すことができ、レスポンスを数百バイトにまで縮小できます。
  2. n>1 を複数の並行した n=1 呼び出しに分割する: n=5 のレスポンスは n=1 のレスポンスの 5 倍のサイズになるため中断されやすく、中断されるとバッチ全体が失われます。分割すれば各レスポンスが小さくなり、失敗しても 1 枚の画像分の損失で済みます。
  3. イグレスを変更する(根本的な解決策): APIYI を呼び出すサービスを回線品質の良いマシン(例えば主流クラウドプロバイダーの BGP やプレミアム回線インスタンス)で実行するか、そのようなマシンを経由してリレーします。また、mtr の結果をデータセンターまたは ISP に提出することもできます。
  4. クライアントの設定: これでパケットロス自体が解決するわけではありませんが、回線が復旧した後の別の問題を防ぐことができます。Java WebClient では、maxInMemorySize のデフォルトはわずか 256 KB ですが、n>1 のレスポンスは 10 MB 以上に達することが多いため、少なくとも 32 MB まで引き上げてください。そうしないと DataBufferLimitException が発生します。responseTimeout は 300 秒以上に設定してください:
再試行する前によくご検討ください。これらの失敗も通常通り課金されます。 接続が切断された時点ですでに画像は生成されており、プラットフォームは配信を完了しているため、コンソールログには通常通り課金された記録が表示されます。再試行するたびに再度生成され、再び支払いが発生します。回線が改善されるまで、やみくもな再試行はコストを増大させるだけです。まずは上記の項目 1〜3 を実施してください。
サポートによる調査が必要な場合は、以下をご提供ください:
  • 失敗が発生した時刻(2026-09-27 13:32 (UTC+8) のように、分単位およびタイムゾーン付き)
  • サーバーのイグレス IP(curl -s ipinfo.io/ip で確認)
  • X-Shellapi-Request-Id レスポンスヘッダー(ステップ 1 のコマンドで出力されます)
  • ステップ 1 の curl -w で出力された行
  • mtr の出力
サーバーと当社の API 間の実際の帯域幅を測定するには、ログには完了と記録されているのにクライアント側で何も受信できない を参照してください。

Node.js: SDK の timeout では届かない 3 つの独立したタイムアウト

Node 18+ の組み込み fetch は undici 上で動作し、リクエストの 3 つの段階をカバーする 3 つの独立したタイムアウト があります。「でもタイムアウトを 5 分に設定したのに」という場合、たいていはそのどれでもない 4 つ目の値を変更しただけです:
openai-node の timeout オプションは AbortController ベースのリクエスト全体のタイムアウトであり、上記 3 つのいずれにも伝播しません。 timeout を 60 秒から 300 秒に増やしても、connectTimeout は 10 秒のままです。素の fetch() における AbortSignal.timeout() も同様です。「タイムアウトを大きくしたのに、まだタイムアウトする」の最もよくある原因はこれです。調整したのが間違ったレイヤーだったのです。

正しい設定

undici の 3 つのタイムアウトを広げるには、グローバルまたはリクエストごとに Agent が必要です:

maxRetries はデフォルトで 2 で、接続エラーを自動再試行します

openai-node は デフォルトで maxRetries: 2 になっており、接続エラーとタイムアウトの両方がその自動再試行の対象です。そのため、1 回の論理的な呼び出しで、コードに再試行ロジックが一切なくても 実際のリクエストが 3 回 発生しえます(それぞれが課金対象かどうかは、「課金への影響」カテゴリのどれに属するかで決まります)。 画像エンドポイントは高コストな同期長時間リクエストなので、maxRetries: 0 を必ず明示的に設定し、再試行ロジックは自分で管理してください。独自のバックオフと試行上限を使ってください。課金ルールは 再試行戦略 にあります。
まず、実際にどのスタック上にいるのかを確認してください: node -v, npm ls openai undici axios node-fetch。UND_ERR_* コードが示すのは、その下で undici が使われていることだけです — OpenAI SDK を使っていることの証明にはなりません。素の fetch() でも同じコードが発生し、素の fetch() には maxRetries がまったくありません。

すでに切断済みの接続を keep-alive で再利用する場合

undici はデフォルトで keep-alive によるコネクションプーリングを有効にします。VPN、NAT、またはプロキシがアイドル状態の接続を黙って回収しても、クライアントはそれに気づかず、次のリクエストでもその接続をプールから取り出します — 書き込みは即座に RST を受け取り、read ECONNRESET として表面化します。 呼び出しの間隔が空くとき、これは ECONNRESET の最も一般的な原因です。また、「エラーが 1 つの時間帯に集中する」ことと「最初の再試行ですら失敗する」ことの両方を説明します。再利用を無効にして確認してください:

ローカルプロキシ / VPN: image endpoint が最初に露出するホップ

APIYI は中国本土内から直接到達でき、プロキシや VPN は不要です(API を使うのにプロキシは必要ですか? を参照してください)。そのため、プロキシをオフにして再テストすることは、あなたが実行できる最も安く、最も情報量の多い単独ステップです。ただし、はっきりさせておくと、プロキシはあくまで最も疑わしい変数であって、確定した根本原因ではありません。下のマトリクスが、実際に障害を切り分けます。
画像 endpoint がテキスト系よりはるかに影響を受けやすい理由は 2 つあります。生成中に 30〜60 秒間まったくバイトが流れないことと、MB 級の body が一気に送られることです。chat endpoint は問題なくて image endpoint だけ失敗する場合、たいていこの 2 つのどちらかです。

fake-ip / ルーティングルールの見逃し

プロキシの fake-ip モードでは、ルールの見逃しによって 198.18.x.x のようなルーティング不能なアドレスに送られ、ちょうど 10 秒の接続タイムアウトが発生します。これは「接続が遅い」のではなく、そもそもルートが存在しないため、connect.timeout を上げても助かりません。実際に到達した remote_ip を必ず記録してください。

生成中にアイドル接続として回収される

リクエスト送信後 30〜60 秒はゼロバイトのままで、プロキシがアイドルポリシーに従って接続を回収します。特徴は、画像サイズに関係なく、失敗時刻が丸い値に収まることです。30 / 60 / 120 秒などです。

MTU / PMTUD ブラックホール

トンネル MTU が経路 MTU を下回っている一方で ICMP の「fragmentation needed」が落とされ、PMTUD が壊れます。典型的な特徴は、小さなリクエストは常に問題なく、大きなレスポンスは常に止まることで、受信済みバイト数は数 KB から数十 KB で固定されます。トンネル MTU を 1400 前後まで下げると、たいてい直ります。

MITM 復号 + 完全バッファリング

HTTPS 復号が有効なプロキシは、大きな body を丸ごとバッファすることが多く、サイズ上限に達する場合があります。あるいは、chunked を Content-Length に書き換えて長さを誤り、RST を発生させることもあります。これも MB 級の image response にのみ起き、テキスト呼び出しには起きません。

診断マトリクス

これがこのセクションの核心です。軸は 2 つだけです。障害が最初の 1 バイトの前に起きたか後に起きたか、そして何バイト到着したかです。 この最後の行が、単独で最も価値の高いテストです。body を数 MB から約 1 KB に縮めます。URL mode で一貫して成功し、base64 mode で一貫して失敗するなら、問題は転送量に比例しています。この場合、最初の 2 列は完全に除外できます。
一番右の列(chunked 終端子の欠落)は、最近は新しい形も取るようになりました。終端子なし、クローズなしで、無期限にハングするという形です。従来の「300 秒後に正常終了してクローズされる」形ではなくなっています。どちらの形でも共通しているのは、データはすでに完了しており、image はそのまま使えることであり、扱いは同じです。クライアント側で response を完了させてください。末尾でハングするリクエスト を参照してください。

フルタイミングプロファイルを 1 コマンドで確認する

マトリクスと照らし合わせると、connect がない → 接続段階; ttfb がなく、丸い total → アイドル回収; full bytes だが total ≈ ttfb + 300 と curl: (18) がある → 終端子の欠落; bytes が数十 KB で止まる → MTU です。
proxy-on と proxy-off の A/B テストを行うときは、実行を交互にしてください — まとめて実行しないでください。 プロキシ経由を 5 回、その後に直結を 5 回という並べ方では、時間窓の障害が結果を汚染して、完全に誤った結論になります。実際に、すべて失敗する区間のあとに数分後にはすべて成功し、また失敗する、という区間を計測しています。代わりに proxy → direct → proxy → direct を実行し、毎回 remote_ip を記録してください。

その他の一般的なトリガー

実行中の手動中断

デバッグ中の 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 を返します。このケースでは課金は発生しません。 同じリクエストでこのエラーが何度続いても、請求書に対応する課金は残りません — この失敗に対して料金を支払うことはありません。

あなたのクライアントが先に切断した場合 — 通常どおり課金されます

もう一方のケースは、あなた側が先に切断したままゲートウェイの配信が完了する場合です。つまり、クライアントのタイムアウト発火、デバッグ中の Ctrl+C、プロセスの再起動、または OOM kill などです。サーバー側と上流での生成はすでに完了しているため、これらは通常どおり課金されます — 「画像を受け取れなかった」ことは「課金されなかった」ことを意味しません。大きな画像リクエストをトラブルシューティングで何度も投げると、実際の請求が積み上がります。
両者を見分ける方法は、まさに上の表のとおりです: コンソールに通常の呼び出しが記録されたか、500 write_response_body_failed が記録されたかを確認してください。 したがって、再試行ポリシーもそれに合わせて節度を保つべきです: トランスポートレベルの失敗は再試行する価値がありますが、再試行のたびに別個に課金される呼び出しになる可能性があります(どちらのカテゴリに当たるかによります)。無制限の再試行ループは決して書かないでください。

正しくリトライする方法

中核ルール: リトライするのはトランスポートレベルの例外だけで、HTTPレベルのエラーは絶対にリトライしません。4xx を 1万回再送しても、依然として 4xx でしかなく、時間の無駄です。
すべての試行を個別に記録してください(上の attempts リスト)。そうしないと、クライアント側のリトライが成功したときに、ログにはきれいな 200 だけが残り、トランスポートが実際に何回壊れたのかが分からなくなります。これはチャネル品質を評価するときに不可欠なデータであり、自分のリトライをチャネルの挙動として誤読するのを防いでくれます。

レスポンスを丸ごと読み込まず、ストリーミングしてください

大きな本文では、stream=True を使ってチャンクごとに読み取ってください。これによりピークメモリを抑えられ、転送のどの段階で壊れたのかを正確に示せます:
その 2 つ目のケースはリトライしないでください: データはすでに完全で、画像はそのまま利用できます。クライアント側の完全な完了コードについては、最後で Requests がハングする問題 を参照してください。

サポートに連絡する価値が本当にある場合

自己確認が済んだら、次のいずれかに当てはまる場合はエスカレーションしてください:
  • 適切な OpenSSL インタープリターに切り替え、シリアル実行に落としても、依然として安定して再現する;
  • 同じ時間帯で他は正常なのに、1つの特定のチャネルまたはモデルだけが失敗する;
  • レスポンスボディは完全に到着している(バイト数がContent-Lengthと一致する)にもかかわらず、接続がタイムアウトするまで閉じない — これは上流で chunked 終端が欠けていることを意味し、チャネル側の問題です。まず Requests Hanging at the End で説明しているクライアント側の完了処理を追加して画像を復旧し、それでも問題が残る場合はチケットを起票してください;
  • エラーが明らかに上流側由来である(upstream_error、生の上流 5xx)。
チケットには、x-request-id、呼び出し時刻(タイムゾーン付き、例: 2026-07-29 14:32 (UTC+8))、モデル名、imageSize などの主要パラメータ、クライアントの生の例外、そして既に完了した自己確認手順を含めてください。

関連ドキュメント

終了時にリクエストがハングする問題

画像は到着したのに接続が終了しない場合の、クライアント側での完了処理

必読 & ベストプラクティス

同期呼び出し、モデルごとのタイムアウト、base64 の扱い、切断時の課金

独自の非同期キューを構築する

同期呼び出しをタスクキューでラップし、再試行でまれなドロップを吸収します

プロキシは必要ですか?

APIYI はプロキシなしで直接接続し、証明書と DNS の問題をセルフチェックします

Gemini 画像エラーハンドリング

Gemini の画像生成におけるエラーコードと finishReason の扱い

エラーの取得

これらのエラーを全文で出力する方法と、サポートチケットに含めるべきフィールド