Laravel『Response Too Large』の原因と対処法

Laravel『Response Too Large』の原因と対処法

Laravelで「Response Too Large」が出る場合、アプリ(Laravel)単体の例外というより、APIゲートウェイ・リバースプロキシ・CDN・ブラウザ・クライアントSDKなど“途中のレイヤ”が「レスポンスが大きすぎる」と判断して落としているケースが多い。巨大なJSON(大量レコード、巨大なネスト、Base64画像の直埋め)、バイナリをJSONで返している、ログやデバッグ情報を混ぜている、無限ループや循環参照で異常に膨らんだレスポンスを生成している、といった原因が典型。まず「どこが」制限しているのか(Laravel / PHP / Nginx / CDN / API Gateway / クライアント)を特定し、サイズを減らすか、配信方式(ダウンロード、ストリーミング、ページング)を変える。

エラーの出方(メッセージ例)と発生条件

「Response Too Large」は環境によって表現が変わる。以下はよくある例。

# 例: API Gateway / Proxy / SDK 側のメッセージ
ResponseTooLargeException
Response too large
Payload Too Large
413 Payload Too Large
upstream sent too big header
client intended to send too large body

発生条件の典型:
・大量データを一括で返す(全件取得、CSVをJSONに詰める、巨大な配列)
・画像/ファイルをBase64でJSONレスポンスに埋め込む
・Eager load しすぎ + リレーションのネストが深すぎて JSON が膨張する
・循環参照(親→子→親)を含む構造をそのまま返して無限に膨らむ/例外になる
・CookieやHeaderが大きすぎて、ヘッダ制限で落ちる(“body”ではなく“header”が原因)
・デバッグツール(Debugbar、Xdebug、dump())の出力が混入してレスポンスが肥大化

まず「どのレイヤで」大きすぎると言われているかを特定する

同じ現象でも、制限している場所が違えば対処も違う。切り分けの軸は「HTTPステータス」と「ログ」。
・413 が返っている → だいたいリバプロ/ゲートウェイ側の制限
・500 でアプリ側ログに巨大配列生成の痕跡 → Laravel側で巨大レスポンス生成
・“upstream sent too big header” → ヘッダ制限(Nginx等)
・クライアントSDKが「Response Too Large」と言う → クライアント側の受信制限の場合もある

確認用コマンド例。

# ステータスとヘッダを確認
curl -i https://example.com/api/items

# レスポンスサイズを計測(概算)
curl -s https://example.com/api/items | wc -c

# 圧縮後サイズ(gzip)も確認
curl -sH 'Accept-Encoding: gzip' https://example.com/api/items --compressed | wc -c

原因1: 全件取得や巨大配列でJSONが肥大化している

例:Eloquentで全件を取得して、そのまま返す。

// NG: 全件をメモリに載せて巨大JSONを返す
Route::get('/users', function () {
    return response()->json(\App\Models\User::all());
});
[/code]

対処は「ページング」「検索条件」「必要カラムだけ」を徹底する。

// OK: ページング + 必要カラムだけ
Route::get('/users', function () {
    return \App\Models\User::query()
        ->select(['id', 'name', 'email'])
        ->orderBy('id')
        ->paginate(50);
});

大量データを返すAPIは、limit/offset か cursor pagination を基本にして、デフォルトで小さく返す。

原因2: リレーションのネスト(with の付けすぎ)で膨らむ

Eager load を増やすほどレスポンスは肥大化する。さらにネストが深いと一気に増える。

// NG: ネストが深すぎる
$orders = Order::with(['user', 'items.product', 'items.product.category', 'payments', 'shipments'])->get();
return response()->json($orders);

対処:
・必要な場面だけ with を付ける
・必要なカラムだけ選ぶ(with の中でも select を使う)
・Resource(JsonResource)で出力を制御する

Resource例。

// app/Http/Resources/OrderResource.php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'total' => $this->total,
            'user' => [
                'id' => $this->user_id,
                'name' => optional($this->user)->name,
            ],
            'items' => $this->items->map(fn($i) => [
                'id' => $i->id,
                'product_id' => $i->product_id,
                'qty' => $i->qty,
            ]),
        ];
    }
}

呼び出し側。

return OrderResource::collection(
    Order::with(['user:id,name', 'items:id,order_id,product_id,qty'])
        ->paginate(30)
);

原因3: Base64画像/ファイルをJSONに直埋めしている

画像やPDFをBase64にしてレスポンスに載せると、サイズが爆増しやすい(Base64は約1.33倍)。

// NG: 画像をBase64で直埋め
return response()->json([
  'image' => base64_encode(Storage::get($path)),
]);

対処:
・署名付きURLを返して、ファイルは別エンドポイントでダウンロードさせる
・S3などに置いてURLだけ返す
・どうしてもAPIで返すなら streamDownload / download を使う

署名付きURL例。

$url = Storage::disk('s3')->temporaryUrl(
    $path,
    now()->addMinutes(10)
);

return response()->json(['url' => $url]);

原因4: 循環参照や意図しない巨大構造を返している

Modelをそのままjson化すると、リレーションやアクセサが連鎖して想定以上に膨らむことがある。親子関係などで循環参照があると危険。
対処:
・Resourceで出力を固定する
・不要なリレーションを隠す($hidden)
・アクセサで巨大データを生成していないか確認する

$hidden例。

// app/Models/User.php
protected $hidden = [
    'password',
    'remember_token',
    'large_blob_column',
];

原因5: ヘッダ(Cookie/Set-Cookie)が大きすぎる

「レスポンスボディ」ではなく「レスポンスヘッダ」が大きすぎて落ちるケースもある。Nginxなら “upstream sent too big header” が典型。
発生条件:
・Cookieに巨大データを詰めている
・セッション/認証の仕組みでSet-Cookieが肥大化している
・ヘッダに長いトークンやデバッグ情報を載せている

対処:
・Cookieにデータを詰めない(IDだけにする)
・セッションドライバを file/redis/database にして中身はサーバ側へ
・レスポンスヘッダのサイズ上限(Nginx等)を見直す(必要なら)

LaravelのセッションをRedisに寄せる例。

# .env
SESSION_DRIVER=redis
CACHE_DRIVER=redis

原因6: ログ/デバッグ出力が混入してレスポンスが巨大化

dump(), dd(), Debugbar、もしくは例外時にHTMLエラーページが返って“見た目はJSONのつもり”が実際は巨大HTMLになっていることがある。
対処:
・本番では APP_DEBUG=false を徹底
・APIは例外ハンドリングでJSONに統一
・ログはレスポンスに混ぜず、サーバ側ログへ

API例外をJSON化する方向性(概念)。

// app/Exceptions/Handler.php のrender内などで、APIならJSONに寄せる
if ($request->expectsJson()) {
    return response()->json([
        'message' => $e->getMessage(),
    ], 500);
}

対処: ページング / チャンク / ストリーミングで“返し方”を変える

大量データを返す必要がある場合、「レスポンスを小さくする」のではなく「分割して返す」「ダウンロードにする」「非同期にする」へ切り替える。
・一覧API → paginate / cursorPaginate
・集計 → DB側で集計して返す(生データを返さない)
・エクスポート → CSV生成してファイルとして返す(ストリーミング)

CSVをストリーミングする例。

use Symfony\Component\HttpFoundation\StreamedResponse;

Route::get('/export/users', function () {
    $response = new StreamedResponse(function () {
        $out = fopen('php://output', 'w');
        fputcsv($out, ['id', 'name', 'email']);

        \App\Models\User::query()
            ->select(['id', 'name', 'email'])
            ->orderBy('id')
            ->chunk(1000, function ($users) use ($out) {
                foreach ($users as $u) {
                    fputcsv($out, [$u->id, $u->name, $u->email]);
                }
            });

        fclose($out);
    });

    $response->headers->set('Content-Type', 'text/csv');
    $response->headers->set('Content-Disposition', 'attachment; filename="users.csv"');

    return $response;
});

対処: 圧縮(gzip/brotli)を有効化して転送量を減らす

JSONは圧縮が効きやすいので、転送量が原因で落ちている場合は改善することがある。ただし“サイズ制限”が圧縮前なのか圧縮後なのかはレイヤによって違う。
確認として、Accept-Encoding を付けたときのサイズを測ると傾向が分かる。

curl -sH 'Accept-Encoding: gzip' https://example.com/api/items --compressed | wc -c

圧縮は「根本の巨大レスポンス」を隠すだけになることもあるので、まずは不要データ削減が優先。

対処: どうしても制限値に当たる場合の設定見直し(どこが制限しているか次第)

503/413/SDK例外など、制限をかけている場所が特定できたら、そのレイヤの上限を見直す選択肢もある。
代表例:
・Nginx: client_max_body_size(主にリクエスト)/ proxy_buffer_size(ヘッダ・バッファ)
・API Gateway / CDN: レスポンス最大サイズ
・PHP-FPM/アプリ: メモリ制限で巨大配列生成が落ちる(memory_limit)

Nginx側の例(概念)。

# request body が大きい場合(アップロード系)
client_max_body_size 20m;

# upstream response header が大きい場合(Cookie肥大など)
proxy_buffer_size 16k;
proxy_buffers 8 16k;

設定変更は“最後の手段”に寄せ、まずレスポンス設計を小さくする方が安定する。

チェックリスト(上から順に確認する)

1) 実際に返っているHTTPステータスは何か(413/500/503など)を確認したか
2) 「どのレイヤ」が大きすぎると言っているか(Laravel/PHP/Nginx/CDN/API Gateway/クライアント)をログで特定したか
3) レスポンスサイズ(curl | wc -c)を測って、どのエンドポイントが巨大化しているか把握したか
4) 一覧系APIで all()/get() の全件返しをしていないか(paginate/cursorPaginateにできないか)
5) with のネストが深すぎないか、Resourceで必要フィールドだけ返しているか
6) Base64をJSONに埋め込んでいないか(URL返却やダウンロードへ切り替えられないか)
7) Cookie/ヘッダが肥大化していないか(upstream sent too big header の兆候がないか)
8) APP_DEBUG=true や dump()/dd() の混入でレスポンスが膨らんでいないか
9) 大量データはストリーミング/非同期/ファイル配布に切り替える設計にできないか