# 機能設計書 41-fetch拡張（データキャッシュ）

## 概要

本ドキュメントは、Next.jsにおけるネイティブfetch APIの拡張機能（データキャッシュ）の設計を記述する。グローバルfetch関数をパッチし、リクエストの重複排除・キャッシュ制御・再検証機能を統合的に提供するための仕組みである。

### 本機能の処理概要

**業務上の目的・背景**：Webアプリケーション開発において、外部APIやデータソースへのfetchリクエストは頻繁に発生する。同一データの重複取得によるパフォーマンス低下やサーバー負荷を軽減し、静的生成（SSG）やISRといったNext.js固有のレンダリング戦略とfetchを統合するために、ネイティブfetch APIを拡張している。

**機能の利用シーン**：Server Components内でのデータ取得、ビルド時の静的ページ生成、ISRによるバックグラウンド再検証、開発時のHMRキャッシュなど、あらゆるデータ取得シーンで自動的に適用される。

**主要な処理内容**：
1. グローバルfetch関数のパッチ（`patchFetch`）によるNext.js固有のキャッシュ制御注入
2. fetch呼び出しの重複排除（`createDedupeFetch`）
3. `next.revalidate`および`next.tags`オプションによるキャッシュ制御
4. IncrementalCacheとの統合によるfetchレスポンスの永続化
5. stale-while-revalidateパターンによるバックグラウンド再検証
6. Dynamic I/O prerenderingにおけるハンギングプロミス制御
7. fetchメトリクスの収集とトレーシング

**関連システム・外部連携**：IncrementalCache（ファイルシステムキャッシュ）、OpenTelemetryトレーシング、HMRキャッシュ（開発時）

**権限による制御**：特になし。ただしDraft Mode有効時はキャッシュがバイパスされる。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 本機能は画面に直接関連しない。サーバーサイドのデータ取得レイヤーで動作する |

## 機能種別

データ連携 / キャッシュ制御

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| input | RequestInfo \| URL | Yes | fetchリクエストのURL又はRequestオブジェクト | URL形式チェック（不正時はネイティブfetchに委譲） |
| init | RequestInit | No | fetchのオプション。Next.js独自の`next`フィールドを含む | - |
| init.next.revalidate | number \| false | No | キャッシュ再検証間隔（秒）。falseは無期限キャッシュ | 0以上の数値またはfalse。不正値はエラー |
| init.next.tags | string[] | No | キャッシュタグ。オンデマンド再検証に使用 | 文字列配列。最大長256、最大128個 |
| init.cache | string | No | 標準のcacheオプション（force-cache, no-store等） | revalidateとの競合チェック |

### 入力データソース

fetch呼び出しの引数から取得。加えてWorkStore/WorkUnitStoreからページレベルのキャッシュ設定（fetchCache）を参照する。

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| Response | Response | 標準のfetch Responseオブジェクト。キャッシュヒット時はキャッシュから復元 |
| FetchMetric | FetchMetric | fetch計測データ（start, end, url, cacheStatus等） |

### 出力先

fetch呼び出し元のServer ComponentまたはRoute Handler。キャッシュデータはIncrementalCache（ファイルシステム/メモリ）に永続化。

## 処理フロー

### 処理シーケンス

```
1. patchFetch() でグローバルfetchをパッチ
   └─ createDedupeFetch()で重複排除ラッパーを作成し、createPatchedFetcher()でキャッシュ制御を注入
2. パッチ済みfetch呼び出し時
   └─ WorkStore/WorkUnitStoreからコンテキスト取得
3. キャッシュ設定の解決
   └─ init.next.revalidate, init.cache, pageFetchCacheMode, autoNoCacheの優先度判定
4. キャッシュキー生成
   └─ incrementalCache.generateCacheKey(url, init)
5. キャッシュルックアップ
   └─ HMRキャッシュ → IncrementalCache の順で検索
6. キャッシュヒット時
   └─ stale判定 → フレッシュならそのまま返却、staleならバックグラウンド再検証を開始してstaleデータを返却
7. キャッシュミス時
   └─ オリジナルfetchを実行し、レスポンスをキャッシュに保存
8. レスポンス返却
   └─ fetchメトリクスを記録してResponseを返却
```

### フローチャート

```mermaid
flowchart TD
    A[fetch呼び出し] --> B{内部fetch?}
    B -->|Yes| C[オリジナルfetchを実行]
    B -->|No| D{WorkStore存在?}
    D -->|No| C
    D -->|Yes| E{Draft Mode?}
    E -->|Yes| C
    E -->|No| F[キャッシュ設定解決]
    F --> G{revalidate値決定}
    G --> H{キャッシュ可能?}
    H -->|No| I[オリジナルfetch実行]
    H -->|Yes| J[キャッシュキー生成]
    J --> K{キャッシュヒット?}
    K -->|Yes - Fresh| L[キャッシュから返却]
    K -->|Yes - Stale| M[staleデータ返却 + バックグラウンド再検証]
    K -->|No| N[オリジナルfetch実行]
    N --> O[レスポンスをキャッシュに保存]
    O --> P[レスポンス返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-41-01 | キャッシュオプション競合 | cacheとrevalidateが同時指定された場合、競合する組み合わせは警告を出し両方を無効化 | cache: force-cache + revalidate: 0 等 |
| BR-41-02 | 自動キャッシュ無効化 | 明示的なキャッシュ設定がない場合、authorization/cookieヘッダーやPOSTメソッドでキャッシュを無効化 | 明示的設定なし + 特定条件 |
| BR-41-03 | ビルド時暗黙キャッシュ | ビルド時のプリレンダリングでは、明示設定がなくても暗黙的にキャッシュを有効化 | isBuildTimePrerendering === true |
| BR-41-04 | revalidate伝播 | fetchのrevalidate値が親スコープのrevalidateより小さい場合、親スコープのrevalidateを更新 | revalidateStore存在時 |
| BR-41-05 | タグ伝播 | fetchに付与されたタグは親スコープ（prerender, cache等）に蓄積される | revalidateStore存在時 |

### 計算ロジック

- `finalRevalidate`の決定: `currentFetchRevalidate` > `pageFetchCacheMode` > `autoNoCache` > `revalidateStore.revalidate` の優先度で決定
- `INFINITE_CACHE`（revalidate: false時）は内部的に`CACHE_ONE_YEAR`（31536000秒）として処理

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| - | - | - | RDBは使用しない。IncrementalCache（ファイルシステム/メモリ）を使用 |

### テーブル別操作詳細

本機能はRDBを使用しない。キャッシュストアとしてIncrementalCacheを使用し、`CachedRouteKind.FETCH`形式でデータを保存する。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | InvalidRevalidate | revalidateに負の数やNaN等の不正値が指定された | エラーをスローし呼び出し元に通知 |
| - | CacheConflict | cacheとrevalidateが矛盾する組み合わせ | 警告を出力し両方の設定をクリア |
| - | FetchCacheViolation | fetchCacheモード制約違反（only-no-storeでforce-cache等） | エラーをスロー |
| - | CacheSetFailure | キャッシュ保存失敗 | console.warnで警告出力し処理を継続 |

### リトライ仕様

fetchリクエスト自体のリトライ機能は本機能に含まれない。stale-while-revalidateによるバックグラウンド再検証がリトライ的役割を果たす。

## トランザクション仕様

トランザクション管理は不要。キャッシュ操作は冪等性を持ち、ロック機構（`incrementalCache.lock`）により同一キーへの並行書き込みを制御する。

## パフォーマンス要件

- キャッシュヒット時はネットワーク遅延なしでレスポンスを返却
- 重複排除により同一レンダリング内の同一URLへのfetchは1回のみ実行
- OpenTelemetryスパンによるfetch実行時間の計測

## セキュリティ考慮事項

- fetchのURL解析時にusernameとpasswordを除去（認証情報の漏洩防止）
- authorization/cookieヘッダーが含まれるリクエストは自動的にキャッシュ無効化
- Draft Mode時はキャッシュをバイパスし、常に最新データを取得
- キャッシュキーにはリクエストのボディ内容も含まれるため、異なるリクエストが同一キャッシュを返すことはない

## 備考

- Edge Runtime環境ではcloudflare Workers互換のためcacheプロパティを削除する処理が含まれる
- Dynamic I/O prerendering時は明示的キャッシュ設定がないfetchはハンギングプロミスを返し、動的レンダリングにフォールバックする
- `NEXT_OTEL_FETCH_DISABLED=1`環境変数でOpenTelemetryスパンを無効化可能

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、キャッシュに保存されるデータの構造とfetch計測データの型を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | types.ts | `packages/next/src/server/response-cache/types.ts` | CachedFetchData型（41-46行目）、CachedFetchValue型（57-64行目）を確認。headers/body/status/urlの構造を理解する |
| 1-2 | base-http.ts | `packages/next/src/server/base-http.ts` | FetchMetric型の定義を確認。start/end/url/cacheStatus等の計測項目 |

**読解のコツ**: `CachedRouteKind.FETCH`がfetchキャッシュエントリの識別子。bodyはbase64エンコードされた文字列として保存される。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | patch-fetch.ts | `packages/next/src/server/lib/patch-fetch.ts` | `patchFetch()`関数（1280-1290行目）がエントリーポイント。`createDedupeFetch`で重複排除し`createPatchedFetcher`でパッチ |

**主要処理フロー**:
1. **1280-1290行目**: `patchFetch()`でグローバルfetchを上書き
2. **258-1276行目**: `createPatchedFetcher()`がパッチ済みfetch関数の本体を生成

#### Step 3: キャッシュ制御ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | patch-fetch.ts | `packages/next/src/server/lib/patch-fetch.ts` | 263-1260行目: パッチ済みfetch関数の内部処理。キャッシュ判定・revalidate値の解決・キャッシュルックアップを行う |

**主要処理フロー**:
- **291-293行目**: WorkStore/WorkUnitStoreからコンテキスト取得
- **342-489行目**: revalidate値とキャッシュモードの解決
- **757-769行目**: キャッシュキー生成
- **938-1061行目**: キャッシュルックアップとstale判定
- **776-932行目**: `doOriginalFetch()`によるオリジナルfetch実行とキャッシュ保存

#### Step 4: キャッシュ保存処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | patch-fetch.ts | `packages/next/src/server/lib/patch-fetch.ts` | `createCachedPrerenderResponse()`（137-176行目）とcreateCachedDynamicResponse()（178-251行目） |

**主要処理フロー**:
- **137-176行目**: プリレンダリング時のキャッシュ保存。レスポンスをバッファしてIncrementalCacheに保存
- **178-251行目**: 動的レンダリング時のキャッシュ保存。ストリーミングレスポンスをクローンして非同期でキャッシュ保存

#### Step 5: 重複排除処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | dedupe-fetch.ts | `packages/next/src/server/lib/dedupe-fetch.ts` | `createDedupeFetch()`（44-129行目）。React.cacheを使ったURL単位の重複排除 |

### プログラム呼び出し階層図

```
patchFetch()
    │
    ├─ createDedupeFetch(globalThis.fetch)
    │      └─ React.cache()ベースのURL単位キャッシュ
    │
    └─ createPatchedFetcher(dedupedFetch, options)
           │
           ├─ validateRevalidate()
           ├─ validateTags()
           ├─ trackFetchMetric()
           ├─ incrementalCache.generateCacheKey()
           ├─ incrementalCache.lock()
           ├─ incrementalCache.get()
           ├─ doOriginalFetch()
           │      ├─ createCachedPrerenderResponse()
           │      │      └─ incrementalCache.set()
           │      └─ createCachedDynamicResponse()
           │             └─ incrementalCache.set()
           └─ markCurrentScopeAsDynamic()
```

### データフロー図

```
[入力]                     [処理]                              [出力]

fetch(url, init) ───▶ createPatchedFetcher ───▶ Response
                           │
                           ├─▶ キャッシュ設定解決
                           ├─▶ キャッシュキー生成
                           ├─▶ IncrementalCache.get()
                           │      │
                           │      ├─ HIT(fresh) ──▶ キャッシュResponse
                           │      ├─ HIT(stale) ──▶ staleResponse + 再検証
                           │      └─ MISS ──▶ originFetch
                           │                    └─▶ IncrementalCache.set()
                           └─▶ FetchMetric記録
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| patch-fetch.ts | `packages/next/src/server/lib/patch-fetch.ts` | ソース | fetch拡張の本体。パッチ適用・キャッシュ制御ロジック |
| dedupe-fetch.ts | `packages/next/src/server/lib/dedupe-fetch.ts` | ソース | fetchリクエストの重複排除 |
| clone-response.ts | `packages/next/src/server/lib/clone-response.ts` | ソース | Responseオブジェクトのクローン（undiciバグ回避） |
| types.ts | `packages/next/src/server/response-cache/types.ts` | ソース | キャッシュデータ型定義 |
| work-async-storage.external.ts | `packages/next/src/server/app-render/work-async-storage.external.ts` | ソース | WorkStore（ページレベルの状態管理） |
| work-unit-async-storage.external.ts | `packages/next/src/server/app-render/work-unit-async-storage.external.ts` | ソース | WorkUnitStore（レンダリング単位の状態管理） |
| dynamic-rendering.ts | `packages/next/src/server/app-render/dynamic-rendering.ts` | ソース | 動的レンダリング判定・ハンギングプロミス |
| constants.ts | `packages/next/src/lib/constants.ts` | ソース | CACHE_ONE_YEAR, INFINITE_CACHE等の定数 |
