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 就恢復播放了。之後遇到同樣的外掛錯誤,我會先檢查影片請求的回應內容,留下紀錄再調整設定。
聊聊你的想法
有不同的做法,或遇到相似的問題?歡迎一起討論。
正在載入留言…
留言與討論