Cloudflare 代理的 MP4 在 Safari 出現外掛錯誤:停用壓縮後,還得清快取

今天處理到一個客戶網站的問題:原本的 MP4 影片突然無法在 iPhone 上播放,但電腦和 Android 都正常。

透過 Safari 的開發者工具檢查時,發現影片載入出現與外掛有關的錯誤訊息。網路上也有人遇到相同情境,Console 顯示:

Failed to load resource: Plug-in handled load

ForthFocus 分享的案例就很接近這次的情況:網站開啟 Cloudflare Proxy,MP4 在電腦和 Android 正常,到了 iPhone 或 iPad 的 Safari 卻無法播放,並出現上面的訊息。

他們在 .htaccess 排除 MP4 的 gzip 或 Brotli 壓縮、調整回應標頭,再清除 Cloudflare 的影片快取,才恢復正常播放。

看到這裡,想到客戶的網站也有開啟 Cloudflare Proxy,我的直覺告訴我:Bingo!應該就是這個問題。

最後讓影片恢復正常的處理,是在主機的 .htaccess 調整設定,讓 MP4 不套用 gzip 等 HTTP 壓縮,再清除 Cloudflare 快取。

這兩個動作做完後,Safari 才能正常播放。回頭整理原因,這次問題很可能與影片的範圍讀取、HTTP 壓縮,以及 Cloudflare 快取的舊回應有關。

不過,目前沒有出錯當下的完整 HTTP 回應紀錄,還無法確認究竟是哪個標頭或哪一層處理出了問題。

Safari 播放影片時,會分段讀取檔案

播放 MP4 時,Safari 會使用 HTTP Range Request,也就是向伺服器要求檔案中的某一段資料,不必每次都下載整支影片。讀取影片資訊、拖曳進度條或接續播放,都可能用到這個機制。

例如,要求檔案最前面的兩個位元組:

Range: bytes=0-1

伺服器正常處理這個請求時,回應可以像這樣:

HTTP/2 206 Partial Content
Content-Type: video/mp4
Content-Range: bytes 0-1/12345678
Content-Length: 2

這裡的 206 Partial Content 表示伺服器回傳的是部分內容。Content-Range 說明這一段在整個檔案中的位置,Content-Length 則是這次回傳的資料長度。

上面的數字只是示意,並非這次網站的實際紀錄。

Apple 的媒體文件也要求,提供給 iOS 的媒體伺服器必須支援 byte-range requests。因此,排查 Safari 影片問題時,除了確認檔案能下載,也要檢查伺服器能否正確回傳其中一段。Apple 官方文件

MP4 套用 HTTP 壓縮後,多了哪些變數?

這次在 .htaccess 關閉的,是傳輸時額外套用的 gzip 等 HTTP 壓縮。影片本身的編碼並沒有因此改變。

HTTP 壓縮會改變傳輸中的資料表示。當範圍讀取、壓縮和代理快取同時存在時,回應的內容、長度與範圍資訊就必須保持一致。

壓縮不代表一定會出錯。但如果其中一層回傳的標頭與內容對不上,播放器就可能無法正確讀取影片。

Cloudflare 也記錄了一種具體情況:如果需要解壓完整回應,可能會忽略 Range,改回 200 與整份檔案。這也是本次可以懷疑的方向,仍要靠實際回應確認。Cloudflare Range 文件

Cloudflare 的文件提到,動態壓縮可能使 Content-Length 被移除。它也會分別處理「主機到 Cloudflare」與「Cloudflare 到瀏覽器」之間的壓縮協商,所以瀏覽器收到的回應,不一定和主機原本送出的形式相同。Cloudflare 壓縮文件

依照這次停用壓縮後恢復播放的結果,我會優先懷疑這段處理影響了 Safari 的範圍讀取。不過,還不能直接下結論說「Safari 不支援 gzip」,或「Cloudflare 一定會把 MP4 壓壞」。

video/mp4 也不在 Cloudflare 的預設壓縮類型清單中。如果影片出現額外壓縮,應該檢查主機的壓縮規則、實際回傳的 Content-Type,以及是否有自訂的 Cloudflare Compression Rules。Cloudflare 壓縮類型與規則說明

在 .htaccess 排除影片壓縮

如果網站使用 Apache,而且主機允許 .htaccess 設定,可以在網站根目錄的 .htaccess 加入下面這段。這次遇到的是 MP4,範例也一併列入常見影片副檔名,方便網站有其他影片格式時使用:

<IfModule mod_setenvif.c>
    # 常見影片檔案不套用 gzip 或 Brotli 壓縮
    SetEnvIfNoCase Request_URI "\.(mp4|m4v|webm|ogv|mov)$" no-gzip=1 no-brotli=1
</IfModule>

SetEnvIfNoCase 會比對請求路徑,副檔名大小寫都適用。符合上面任一副檔名時,no-gzip 告訴 mod_deflate 停用 gzip,no-brotli 則告訴 mod_brotli 停用 Brotli。Apache mod_deflate 文件、mod_brotli 文件

這裡列入 MP4、M4V、WebM、OGV、MOV 五種格式。排除 HTTP 壓縮不會改變影片編碼,Safari 能否播放仍取決於格式與編碼是否受支援。

存檔後,到 Cloudflare 的 Caching → Configuration → Purge Cache,使用 Custom Purge 清除受影響的影片完整 URL。接著用 iPhone Safari 重新開啟原本無法播放的 MP4,確認能播放,也能拖曳進度條。

為什麼改完 .htaccess,還要清除 Cloudflare 快取?

這次容易漏掉的地方,就是主機設定改好後,Cloudflare 仍可能提供先前快取的影片回應。

網站開啟 Proxy 後,請求會經過 Cloudflare。如果請求命中快取,Cloudflare 可以直接回傳既有內容,不必每次都向主機重新取得。

因此,修改 .htaccess 只代表主機後續產生的回應改變了,Cloudflare 裡已經存在的快取不一定會立刻更新。

最符合這次現象的推測是:

Cloudflare 的 Purge Cache 清除的是資源快取。Cloudflare 不會直接讀取或快取主機上的 .htaccess 設定檔。

還有一個可能影響 Safari 的因素:ETag

ETag(Entity Tag)是伺服器放在 HTTP 回應標頭裡的版本識別碼,可以把它想成檔案的「版本標籤」。瀏覽器或 CDN 再次確認快取時,可以把之前收到的 ETag 帶回伺服器比對。如果版本沒變,伺服器就能回傳 304 Not Modified,讓它沿用快取,省下重新下載檔案的流量。

ETag 又分成強、弱兩種。強 ETag 相同,代表內容的每個位元組都相同。弱 ETag 表示內容可以視為等同,但實際傳輸的位元組不一定完全相同。影片分段讀取需要精確對應檔案位置,因此這個差異就可能影響範圍請求的版本驗證。

帶有 W/ 前綴的就是弱 ETag,例如:

ETag: W/"abc123"

整理資料時,也查到 Cloudflare 有一篇專門說明 MP4 在 Safari 和 iOS 無法播放的文件。

文件指出,Cloudflare 快取層處理範圍請求時,如果涉及弱 ETag,Safari 可能拒絕相關的快取回應,造成影片無法載入或黑畫面。Cloudflare 官方問題說明

這是後續遇到相同問題時可以檢查的方向,但目前沒有證據證明這次就是 ETag 造成的。停用壓縮並清除快取,可能同時改變多個回應條件,不能只從「修好了」反推唯一原因。

下次可以怎麼查?

如果再遇到類似情況,我會先留下修改前後的 HTTP 回應,確認 Range Request 是否正常。

以下指令適用於 macOS 或 Linux 終端機。它會要求影片的前兩個位元組,印出回應標頭,並丟棄下載內容:

curl -sS -D - -o /dev/null \
  -H 'Range: bytes=0-1' \
  -H 'Accept-Encoding: gzip, br' \
  'https://example.com/video.mp4'

把網址換成實際的 MP4 URL。先看是否收到 206 和正確的 Content-Range,再看是否仍有 Content-Encoding。其餘標頭可用來比對修改前後的差異:

項目 檢查重點
HTTP 狀態碼 這個範圍請求是否得到 206 Partial Content
Content-Range 是否正確標示 bytes 0-1/檔案總長度
Content-Length 若成功回傳這段 206,且有此標頭,應為 2
Content-Type 是否為 video/mp4
Content-Encoding 排除影片壓縮後,是否仍出現 gzip 或 br
ETag 是否帶有 W/,修改前後是否改變
CF-Cache-Status 是否命中 Cloudflare 快取,例如 HIT

也可以把指令中的 Accept-Encoding: gzip, br 換成 Accept-Encoding: identity 再跑一次,要求不壓縮的回應。比較兩次的狀態碼、Content-Range 與 Content-Encoding,看看壓縮協商是否影響分段讀取。

HIT 只表示命中快取,看到弱 ETag 的 W/ 也不能單獨判定故障,還要和播放結果及其他標頭一起看。

這裡要使用實際的 GET 請求。只用 curl -I 取得 HEAD 回應,無法確認伺服器是否真的正確回傳那一段影片資料。

修正後也可以再測幾次,觀察重新快取後是否仍能播放,並在 Safari 實際拖曳進度條。這樣才能確認首次載入與後續範圍讀取都正常。

這次在主機排除 MP4 的 HTTP 壓縮,再清除 Cloudflare 快取後,Safari 就恢復播放了。之後遇到同樣的外掛錯誤,我會先檢查影片請求的回應內容,留下紀錄再調整設定。

聊聊你的想法

有不同的做法,或遇到相似的問題?歡迎一起討論。

Email 不會公開,其他讀者看不到;僅用於回覆通知,可隨時取消。

免登入 · 送出後直接公開

留言與討論

正在載入留言…