解決 Laravel 回傳 Etag Header 被 Nginx 攔截的問題

NginxLaravel

HTTP Cache 裡面的其中一個機制是 Etag,可以回傳當前資源的唯一 Hash 值,然後在之後的請求可以比對此 Etag 值有沒有變化,沒變化就直接回傳 304,有改變才會重新下載新的資源。通常如果是靜態資源可以直接交給 Nginx 處理就行了,但剛好我有個需求需要在 Laravel 中處理後才回傳出去,因此就需要在 Laravel 處理 Etag 和 304 了。

Etag 和 If-None-Match

先上一個基本的 Controller,把檔案內容讀出來之後用 md5() Hash 過傳給 Etag Header。然後在第二次請求的時候,會附上 If-None-Match Header,這時就可以比對 Hash 值是否一樣:

class AssetController extends Controller
{
    public function __invoke(Request $request)
    {
        $cacheControl = 'no-cache';

        $content = '...';

        $etag = md5($content);

        if ($this->matchesCache($etag)) {
            return response('', 304, [
                'Cache-Control' => $cacheControl,
            ]);
        }

        $headers = [
            'Content-Type' => $contentType,
            'Cache-Control' => $cacheControl,
            'Etag' => $etag,
        ];

        return response($content, 200, $headers);
    }

    protected function matchesCache($etag)
    {
        /** @var string|null */
        $ifNoneMatch = request()->header('If-None-Match');

        return $ifNoneMatch !== null && $ifNoneMatch === $etag;
    }
}

在本地還可以正常跑,但一搬到線上就沒有效果了。經過一番調查,是 Nginx 開啟 gzip 後就會過濾掉 etag,可是不完全對。它會刪掉不符合規則的強 Etag,而不會管弱 Etag。

簡單來說在這裡我們只要轉成弱 Etag 就行了,弱 Etag 的格式是 W/"etag內容",把 etag內容 前後加上 W/"..." 變成 W/"etag內容" 就可以了:

$etag = md5($content);
// 原本的 Etag: abc123

$etag = 'W/"'.md5($content).'"';
// 新的弱 Etag: W/"abc123"

參考資料