Next.js の middleware や API Route でレスポンスにヘッダーや Cookie を付与したいとき、素の Response
を展開してはいけません。
理由はシンプルで、Response の body が ReadableStream だからです。
Response の body = ReadableStream
fetch
が返す Response
オブジェクトの body
プロパティは、単なる文字列や JSON ではなく ReadableStream になっています。
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
console.log(res.body);
// → ReadableStream { locked: false }
ストリームは一度しか読めない
ReadableStream は「順番に流れてくるデータ」を扱う仕組みです。
一度読み出すと 消費されて空になるため、再利用はできません。
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const text1 = await res.text(); // 1回目はOK
const text2 = await res.text(); // ❌ 空っぽになる or エラー
内部で何が起きているのか
ReadableStream には以下の状態があります:
- queue : ネットワークから届いたデータの一時置き場
-
locked : 読み取り中かどうか(
getReader()
で true) - closed : 全部読み終わった状態(再オープン不可)
res.text()
や res.json()
を呼ぶと:
- queue に溜まったチャンクを順番に消費
- 読み取り中は
locked = true
- 最後まで読むと
closed
→ 以降は再利用不可
このようにReadableStreamは「ライフサイクル」のような性質を持つオブジェクトで、
それがfetch後のレスポンスボディの構造となっています。
検証コードで確認
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const body = res.body;
console.log("読み取り前:", body.locked); // false
const reader = body.getReader();
console.log("getReader後:", body.locked); // true
while (!(await reader.read()).done) {}
console.log("ストリーム読み切った");
const { done } = await reader.read();
console.log("done:", done); // true → closed 状態
読み取り開始で locked = true、最後まで読んだら done = true (closed) になるのが確認できる。
Next.js での落とし穴
middleware で Response
をそのまま返すとき、もし先に body を展開すると空のレスポンスしか返りません。
export async function middleware(req: NextRequest) {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const data = await res.json(); // ← body を消費
return res; // ❌ クライアントには空
}
解決策:NextResponse を使う
NextResponse.json()
なら、データをシリアライズして 新しいレスポンスを生成できるため問題ありません。
import { NextResponse } from "next/server";
export async function middleware(req: NextRequest) {
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const data = await res.json(); // ここで1回だけ読む
return NextResponse.json(data); // ✅ 新しい Response を返す
}
まとめ
- Response の body は ReadableStream(消費型)
- 一度読み始めると locked → closed になり、再利用できない
- middleware で Response を直接返すと空になる落とし穴がある
- そのため Next.js では NextResponse を使って新しいレスポンスを作り直すのが正解