SvelteKitのhandleFetchフックでサーバー側fetchをカスタマイズした話

SvelteKitの handleFetch フックを使うと、load 関数内で実行されるサーバー側の fetch をインターセプトしてリクエストを変更できます。


handle vs handleFetch の使い分け

handle handleFetch
対象 ブラウザからのリクエスト全体 load 関数内の fetch
タイミング すべてのHTTPリクエストに介入 サーバー側fetchにだけ介入
主な用途 認証・ロギング・レスポンス加工 fetchのURL書き換え・ヘッダー付与
ブラウザ → /fetch-demo
               ↓
          【handle】← ここで介入
               ↓
          load関数が実行される
               ↓
          load関数の中でfetch('/api/posts')
               ↓
          【handleFetch】← ここで介入
               ↓
          fetchが実行される

実装例

load 関数内でfetchを使うページ

// src/routes/fetch-demo/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const res = await fetch('/api/posts');
  const posts = await res.json() as { id: number; title: string }[];
  return { posts };
};

res.json()await が必要なのは、fetchが2段階の非同期処理だからです。

  • await fetch() → HTTPレスポンスのヘッダーが届くまで待つ
  • await res.json() → レスポンスの**body(中身)**を読み込むまで待つ

handleFetch でヘッダーを追加する

// src/hooks.server.ts
import type { Handle, HandleFetch } from '@sveltejs/kit';

export const handleFetch: HandleFetch = async ({ request, fetch }) => {
  console.log('[handleFetch] URL:', request.url);

  const newRequest = new Request(request, {
    headers: {
      ...Object.fromEntries(request.headers), // 既存ヘッダーをすべてコピー
      'X-Internal-Request': 'true',           // 新しいヘッダーを追加
    },
  });

  return fetch(newRequest);
};

Object.fromEntries(request.headers) の役割

request.headersHeaders オブジェクトなので、そのままスプレッドできません。Object.fromEntries() でプレーンなオブジェクトに変換してからスプレッドします。

// request.headersの中身のイメージ
// Headers { 'accept': '*/*', 'content-type': 'application/json', ... }

Object.fromEntries(request.headers)
// → { 'accept': '*/*', 'content-type': 'application/json', ... }

これをしないと、元のリクエストが持っていた Content-TypeAccept などのヘッダーが失われます。

// ❌ 既存ヘッダーが消える
new Request(request, {
  headers: { 'X-Internal-Request': 'true' }
})

// ✅ 既存ヘッダーを保持しつつ追加
new Request(request, {
  headers: {
    ...Object.fromEntries(request.headers),
    'X-Internal-Request': 'true',
  }
})

追加したヘッダーの確認方法

handleFetch が追加するヘッダーはサーバー → サーバー間のリクエストに付くため、ブラウザのNetworkタブには表示されません。受信側の +server.ts でログを出して確認します。

// src/routes/api/posts/+server.ts
export const GET: RequestHandler = async ({ request, platform }) => {
  // handleFetchで付けたヘッダーを確認
  console.log('X-Internal-Request:', request.headers.get('X-Internal-Request'));
  // ...
};

主なユースケース

認証ヘッダーを自動付与

export const handleFetch: HandleFetch = async ({ request, fetch, event }) => {
  if (request.url.startsWith('https://api.example.com')) {
    return fetch(new Request(request, {
      headers: {
        ...Object.fromEntries(request.headers),
        'Authorization': `Bearer ${event.locals.user?.token}`,
      },
    }));
  }
  return fetch(request);
};

URLの書き換え

export const handleFetch: HandleFetch = async ({ request, fetch }) => {
  if (request.url.startsWith('https://myapp.example.com/api/')) {
    const url = request.url.replace(
      'https://myapp.example.com/api/',
      'http://localhost:3000/api/'
    );
    return fetch(new Request(url, request));
  }
  return fetch(request);
};

まとめ

  • handleFetchload 関数内の fetch のみに介入する(+server.ts やアクション内のfetchには効かない)
  • 追加したヘッダーはブラウザのNetworkタブでは見えない(サーバー内部の通信のため)
  • 既存ヘッダーを保持するには Object.fromEntries(request.headers) でコピーする
← トップページに戻る