Supabaseの”TypeError: fetch failed”エラーにハマった話とその解決策

Supabaseの”TypeError: fetch failed”エラーにハマった話とその解決策

Supabaseを使っていると、ある日突然現れるのがこのエラー:

TypeError: fetch failed

見た瞬間、「え?何のこと?Supabaseが落ちてる?ネットの問題?」と思った方、安心してください。私も見事にハマりました。この記事では、この不可解なエラーの正体と、その原因・対処法を完全に解説します。


結論:これはSupabaseのエラーではなく「fetch自体が失敗」している

Supabaseは内部的に PostgREST という仕組みを使っており、.select() や .insert() の操作は実際には HTTPリクエストで行われています。そのため、次のような状況になると、リクエストが届く前に失敗するのです。


SupabaseはDBっぽいけど「REST API経由のDB」だという現実

SupabaseはPostgreSQLを使ってるからこそ、「普通のDBと同じ感覚」で操作してしまいがちです。

  • .select().in() は SQLの IN に見えるけど、実態は URLに埋め込まれるGETリクエスト
  • .insert() は INSERT INTO に見えるけど、実態は JSONでPOSTされるHTTPボディ
  • どちらも 通信制限が存在し、DBとは違うボトルネックに直面します

「DB感覚」を超えていく設計力が求められる

  • 100件以上の .in() や .insert() は、チャンク化または RPC(SQL関数)に逃がすなど、REST APIとしての限界を考慮した設計が必要になります。
  • Supabaseは便利だけど、完全にPostgreSQLのようには振る舞わない、という前提を持つことが重要です。

よくある”fetch failed”の原因

1. .in()で配列を渡しすぎた

await supabase
  .from("xp_transactions")
  .select("source_id")
  .in("source_id", hugeArray); // ← IDが500件とかあるとアウト

この場合、GETリクエストのURLに全IDが埋め込まれてしまい、URL長が8KB〜16KBを超えると fetch がクラッシュします。

2. .insert()や.rpc()に大量のデータを渡した

await supabase.from("logs").insert(hugeArray);

bodyサイズが大きすぎて、413 Payload Too Large や fetch failed が発生することがあります。

3. 環境変数ミスでSUPABASE_URL = ""になっていた

createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
  process.env.SUPABASE_SERVICE_ROLE_KEY ?? ""
);

→ 空文字でfetchすると「fetch failed」。実は 環境変数の設定ミスが原因。


対策:どうすればハマらないのか?

1. .in()は100件未満に制限する or 分割する

URL長が8KB〜16KBを超えると fetch がクラッシュするので、それ以下におさまるようにchunk化します。

function chunkArray<T>(arr: T[], size: number): T[][] {
  return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
    arr.slice(i * size, i * size + size)
  );
}

const chunks = chunkArray(hugeArray, 100);
for (const chunk of chunks) {
  await supabase.from("xp_transactions").select("source_id").in("source_id", chunk);
}

3. Supabase RPC(ストアドファンクション)でPOSTに逃がす

.rpc() を使うとリクエストが POST になり、配列などのデータが URL ではなくリクエストボディとして送られます。そのため、.in() のような URL長制限の問題を回避でき、数百KB〜数MBまでのデータを送れるようになります。

-- SQLエディタで定義
create or replace function get_data(ids uuid[])
returns table(id uuid) as $$
  select id from my_table where id = any(ids);
$$ language sql;
await supabase.rpc("get_data", { ids: hugeArray });

まとめ

このエラー、エラーメッセージがあまりにもわかりづらすぎて、根本原因に気づくまでにすごく時間がかかります

もし同じようにハマった方がいたら、この知見が少しでも役に立てば嬉しいです。


おまけ:Supabase用のチャンク処理ユーティリティ

Supabaseで.in()を使うクエリに大量のIDを渡したいときに便利な汎用関数です。

export async function executeChunkedQuery<T>(
  ids: string[],
  queryFn: (
    chunkIds: string[],
  ) => Promise<{ data: T[] | null; error: PostgrestError | null }>,
  chunkSize = 50,
): Promise<{ data: T[] | null; error: Error | null }> {
  if (ids.length === 0) return { data: [], error: null };
  const chunks = chunkArray(ids, chunkSize);
  const allResults: T[] = [];

  for (let i = 0; i < chunks.length; i++) {
    const { data, error } = await queryFn(chunks[i]);
    if (error) return { data: null, error: new Error(error.message) };
    if (data) allResults.push(...data);
  }

  return { data: allResults, error: null };
}

// 使用例
const { data, error } = await executeChunkedQuery(
  userIds, // 500件以上あってもOK
  (chunk) =>
    supabase
      .from("users")
      .select("*")
      .in("id", chunk)
);

Anycloudではプロダクト開発の支援を行っています

プロダクト開発をお考えの方はぜひAnycloudにご相談ください。

まずは相談する

記事を書いた人

村井 謙太

代表取締役

村井 謙太

Twitter

東京大学在学中にプログラミング学習サービスのProgateを立ち上げ、CTOとしてプロダクト開発に従事。 Progate退任後に株式会社Anycloudを立ち上げ、現在は多数のクライアントの技術支援を行っている。