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.headers は Headers オブジェクトなので、そのままスプレッドできません。Object.fromEntries() でプレーンなオブジェクトに変換してからスプレッドします。
// request.headersの中身のイメージ
// Headers { 'accept': '*/*', 'content-type': 'application/json', ... }
Object.fromEntries(request.headers)
// → { 'accept': '*/*', 'content-type': 'application/json', ... }
これをしないと、元のリクエストが持っていた Content-Type や Accept などのヘッダーが失われます。
// ❌ 既存ヘッダーが消える
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);
};
まとめ
handleFetchはload関数内のfetchのみに介入する(+server.tsやアクション内のfetchには効かない)- 追加したヘッダーはブラウザのNetworkタブでは見えない(サーバー内部の通信のため)
- 既存ヘッダーを保持するには
Object.fromEntries(request.headers)でコピーする