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

Supabaseを使っていると、ある日突然現れるのがこのエラー:
TypeError: fetch failed
見た瞬間、「え?何のこと?Supabaseが落ちてる?ネットの問題?」と思った方、安心してください。私も見事にハマりました。この記事では、この不可解なエラーの正体と、その原因・対処法を完全に解説します。
目次
- 結論:これはSupabaseのエラーではなく「fetch自体が失敗」している
- SupabaseはDBっぽいけど「REST API経由のDB」だという現実
- 「DB感覚」を超えていく設計力が求められる
- よくある”fetch failed”の原因
- 1. .in()で配列を渡しすぎた
- 2. .insert()や.rpc()に大量のデータを渡した
- 3. 環境変数ミスでSUPABASE_URL = ""になっていた
- 対策:どうすればハマらないのか?
- 1. .in()は100件未満に制限する or 分割する
- 3. Supabase RPC(ストアドファンクション)でPOSTに逃がす
- まとめ
- おまけ: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)
);