起因
客戶反應,掛在 CDN 的網站用手機無法打開,電腦版可以,用其他 CDN 則沒有這個問題。
團隊第一時間的反應是「怎麼可能會有這種事?」
但…就是會有這種事,也才產生了這篇解題記錄。
重現 & 調查
先來試著重現問題吧。
嘗試用 iPhone Safari 開啟,還真的打不開,僅出現一個不清不楚的錯誤畫面。
用 macOS Safari 也可以重現,錯誤訊息多了一點點。
使用 curl 來測試,也能得到錯誤訊息,而且就明確的多了
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
* http2 error: Invalid HTTP header field was received: frame type: 1, stream: 1, name: [upgrade], value: [h2]
* HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
* stopped the pause stream!
* Connection #1 to host 99czzx.com left intact
curl: (92) HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
試著直接將 request 發到源站,得到如下的 Header:
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Thu, 30 Sep 2021 12:18:33 GMT
Date: Thu, 30 Sep 2021 12:18:33 GMT
< Server: Apache
Server: Apache
< Upgrade: h2
Upgrade: h2
< Connection: Upgrade, close
Connection: Upgrade, close
< Content-Type: text/html; charset=utf-8
Content-Type: text/html; charset=utf-8
根據搜集到的資料, Upgrade: h2 可能是問題的原因。
因為經過 CDN 之後 header 是有可能變動的;著手檢查客戶說沒問題的 CDN 廠商,一樣存在著 Header Upgrade: h2 沒有被修改或是隱藏,那為什麼就可以打開呢?
用 curl 仔細比較 response header ,發現了一些差異:
# curl --verbose
## 正常連線的CDN
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
...中略
> HEAD / HTTP/1.1
> Host: xxxxx.com
> User-Agent: curl/7.64.1
> Accept: */*
## 無法連線的CDN
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
...中略
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7f89a300a400)
> HEAD / HTTP/2
> Host: xxxxx.com
> User-Agent: curl/7.64.1
> Accept: */*
謎底揭曉了,原來是 http/1.1 跟 http/2 的差異。
根據 RFC 7540 ,已經是 HTTP/2 的連線是不能再 upgrade 到 h2 的,故透過我們的 CDN 不能連線其實是正確的結果。
解決方式
從源站就拿掉該 header ,這應該是最合理的解法了。
客戶使用 CDN ,理論上伺服器就不再需要直接對外服務,從CDN 網路連到源站走的也是非加密的 HTTP ,故請客戶評估後拿掉該 Header 即可。
換個角度來看:對客戶來說,A 服務可以用,B 服務不能用也是個事實,也看看如果真要從服務下手能怎麼解吧:
- 關閉 Nginx 的 HTTP/2 支援,根據找到的資料,在同一個 port 上只能選擇全開或全關,而不能依 server_name 設定,為了一個特例要強迫所有客戶用 http/1.1 不太合理,不考慮
- 另一個作法是改 Header ,判斷在特定條件下用 proxy_hide_header Upgrade 來把 Upgrade 給隱藏。
為什麼 Chrome 可以開?
找到原因之後其實滿單純的,但有一件事仍然沒有結果:
如果以 Chrome 開啟該網站,會使用 HTTP/2 protocol ,上述的 Upgrade: h2 也還是存在,但卻是可以正常開啟的。
沒有找到確切的說明,自己猜測是 Chrome 沒有嚴格遵守 RFC 7540 的限制,自行作了一些處理讓 user 可以正確存取網站。
歪打正著
這次的事件,在我們的服務打不開其實是正確的結果,只是剛好…
- 其他 CDN 使用 HTTP/1.1,可以開啟。
- 市佔率最高的 Chrome 也可以開啟。
從客戶的角度看起來就是產品壞掉了。
原先我以為只有前端工程師比較需要了解各 browsers 的差異,沒想到在 Nginx 的世界也會遇到相關的問題啊~
留言