起因

客戶反應,掛在 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 可以正確存取網站。

歪打正著

這次的事件,在我們的服務打不開其實是正確的結果,只是剛好…

  1. 其他 CDN 使用 HTTP/1.1,可以開啟。
  2. 市佔率最高的 Chrome 也可以開啟。

從客戶的角度看起來就是產品壞掉了。

原先我以為只有前端工程師比較需要了解各 browsers 的差異,沒想到在 Nginx 的世界也會遇到相關的問題啊~

參考資料

Website not working in any browser on iOS – DEV Community

最後修改日期: 2021-10-13

作者

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。