【解決済み】Supabase + RenderでError: connect ENETUNREACH(IPv6)エラーが出たときの直し方

Androidアプリ

Node.js(Express)で構築したAPIをRenderにデプロイし、SupabaseのPostgreSQLに接続しようとしたところ、以下のようなエラーでDBへの書き込み(INSERT)が失敗する事象に遭遇しました。

Error inserting product: Error: connect ENETUNREACH 2406:da1a:xxx:xxx:xxx:xxx:xxx:xxx:5432 - Local (:::0)

APIのレスポンスとしては 500 Internal Server Error が返るだけで、クライアント側からは原因が全く分からない状態でした。この記事では、このエラーの原因と解決方法を解説します。同じ構成(Supabase + Render / Vercel / Railway など)で開発している方の参考になれば幸いです。

発生していた事象

Expo(React Native)製アプリからバーコードをスキャンし、ExpressベースのAPI(Renderにデプロイ)経由でSupabaseのPostgreSQLに商品情報をINSERTする、という機能を実装していました。

該当のエンドポイントはこのようなシンプルなコードです。

ts

router.post("/", async (req: Request, res: Response): Promise<any> => {
  const { jancode, name } = req.body;
  try {
    const dbResponse = await pool.query(
      "INSERT INTO products (jancode, name) VALUES ($1, $2) RETURNING *",
      [jancode, name],
    );
    res.json(dbResponse.rows[0]);
  } catch (error) {
    console.error("Error inserting product:", error);
    res.status(500).json({ error: "Failed to insert product" });
  }
});

アプリ側の動作確認では、ローカルへのデータ保存(AsyncStorage)は成功するのに、サーバー側のデータベースには一切登録されない、という状態でした。クライアント側のエラーログには、

商品登録に失敗しました: [Error: HTTP error! status: 500]

としか出ず、これだけでは原因の特定ができません。

エラーメッセージの見方

500エラーの内容を特定するために、まず確認すべきはクライアント側ではなくサーバー側の標準出力ログです。今回のコードでは、DB接続やクエリでエラーが発生した場合、catch ブロックで console.error によりエラー内容が出力される設計になっています。

Renderのダッシュボードでログを確認したところ、次のスタックトレースが出力されていました。

Error inserting product: Error: connect ENETUNREACH 2406:da1a:xxx:xxx:xxx:xxx:xxx:xxx:5432 - Local (:::0)

ENETUNREACH は「Network is unreachable(ネットワークに到達できない)」を意味するエラーコードです。ポイントは、接続先のホストが 2406:da1a:... というIPv6アドレスである点です。

原因:SupabaseのDirect ConnectionはIPv6専用

Supabaseは2024年ごろから、プロジェクトのデフォルトのデータベースホスト名(Direct Connection、db.xxxxx.supabase.co の形式)がIPv6アドレスに解決される仕様になっています。

一方で、RenderをはじめとするPaaS(Railway、Vercel Functions、一部のVPS環境なども含む)では、アウトバウンド(外向き)通信がIPv4のみに制限されている場合があります。この場合、DNSの名前解決自体はIPv6アドレスを返してくるものの、実際にそのアドレスへ接続しようとした瞬間に ENETUNREACH で失敗します。

つまり今回のケースは、

  • アプリケーションのコードは正しい
  • データベースの認証情報も正しい
  • しかし ホスティング環境とDB接続先のネットワーク層(IPv4 / IPv6)が噛み合っていない

というインフラ起因の問題でした。

なぜクライアント側からは500としか見えないのか

try / catch でDBエラーを拾い、一律で 500 を返す実装だったため、クライアント側からは「サーバー内部で何かが失敗した」以上の情報が得られませんでした。

さらに厄介だったのは、アプリ側の実装が次のようになっていた点です。

ts

if (needsRegistration && barcode) {
  try {
    await addProduct(barcode, name.trim());
  } catch (error) {
    console.error("商品登録に失敗しました:", error);
    // 登録に失敗してもローカルへの保存は続行する
  }
}

DB登録に失敗してもエラーを握りつぶし、ローカル保存だけは成功させる設計だったため、アプリの見た目上は正常に動作しているように見えてしまうという点も発見を遅らせる要因になりました。

こうした「一部だけサイレントに失敗する」不具合は、必ずサーバー側のログを一次情報として確認することが早期解決の近道です。

解決方法:Connection Poolerを使う

Supabaseでは、Direct Connectionとは別にConnection Pooler(PgBouncer経由の接続)が用意されており、こちらはIPv4に対応したホスト名で提供されています。IPv6非対応の環境からアクセスする場合は、このPooler経由の接続文字列を使うのが標準的な解決策です。

Poolerには用途に応じて2種類のモードがあります。

モードポート用途
Transaction pooler6543通常のWebアプリ・APIサーバー向け。接続を使い捨てる短命なクエリに最適
Session pooler5432セッション状態を維持する必要がある処理(一部のORM機能など)向け

一般的なREST APIでの単発クエリであれば、Transaction poolerで問題ありません。

設定手順

  1. Supabaseダッシュボードにログインし、対象プロジェクトを開く
  2. 左メニューから Project Settings → Database を開く
  3. Connection string のセクションで「Transaction pooler」(または環境によっては「Connection pooling」)を選択
  4. 表示された接続文字列をコピーする

接続文字列は以下のような形式になります。

postgresql://postgres.xxxxx:[YOUR-PASSWORD]@aws-0-ap-northeast-1.pooler.supabase.com:6543/postgres

Direct Connectionとの違いは、

  • ホスト名が db.xxxxx.supabase.co ではなく aws-0-<region>.pooler.supabase.com になる
  • ユーザー名が postgres ではなく postgres.xxxxx(プロジェクトIDを含む)になる
  • ポート番号が 5432 ではなく 6543(Transaction poolerの場合)になる

の3点です。この接続文字列を、Renderの環境変数 DATABASE_URL に設定し直すだけで完了です。

ts

// api/db.ts(変更なし)
import { Pool } from "pg";
import dotenv from "dotenv";

dotenv.config();

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: false,
  },
});

アプリケーションコード側の修正は一切不要で、環境変数を差し替えるだけで解決します。

修正後の動作確認

環境変数を更新後、Renderを再デプロイしてから改めてバーコードスキャン→商品登録の動作確認を行ったところ、問題なくデータベースへのINSERTが成功しました。

念のため、curl で直接APIを叩いて疎通確認をしておくと確実です。

bash

curl -X POST https://your-api-url/api/addProduct \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_TOKEN" \
  -d '{"jancode":"1234567890123","name":"テスト商品"}'

正常なレスポンスが返ってくれば修正完了です。

まとめ

  • ENETUNREACH エラーの直後にIPv6アドレスが表示されている場合は、ホスティング環境とDBのネットワーク層の不一致を疑う
  • Supabaseの Direct Connection(db.xxxxx.supabase.co)はIPv6専用アドレスに解決される
  • Render をはじめIPv4のみ対応のホスティング環境からは、Direct Connectionには接続できない
  • 解決策は Supabase の Connection Pooler(Transaction pooler / Session pooler)の接続文字列に切り替えること
  • アプリケーションコードの変更は不要で、DATABASE_URL の差し替えのみで解決する
  • こうした「一部の処理だけサイレントに失敗する」不具合は、クライアント側のエラーメッセージだけでなく、必ずサーバー側のログを確認することが早期解決の鍵になる

同じ構成(Supabase × Render/Railway/Vercel等)で開発をしている方は、DB接続エラーに遭遇した際、まず接続文字列がDirect ConnectionかPoolerかを確認してみることをおすすめします。

コメント

タイトルとURLをコピーしました