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

> Supabaseの「TypeError: fetch failed」エラーの原因と対処法について詳しく解説した記事です。このエラーの正体や、具体的な対策を紹介し、効率的なデータ操作のための設計力を高める手助けをします。

- 公開日: 2025-06-11
- 更新日: 2025-06-11
- 著者: 村井 謙太
- タグ: Supabase
- URL: https://tech.anycloud.co.jp/articles/supabase-fetch-failed

---

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

```plaintext
TypeError: fetch failed
```

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

* * *

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

Supabaseは内部的に [PostgREST](https://postgrest.org/) という仕組みを使っており、.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()で配列を渡しすぎた**

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

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

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

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

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

### **3\. 環境変数ミスでSUPABASE\_URL = ""になっていた**

```plaintext
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化します。

```typescript
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
-- 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;
```

```typescript
await supabase.rpc("get_data", { ids: hugeArray });
```

* * *

## **まとめ**

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

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

* * *

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

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

```typescript
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)
);
```
