用子路徑掛服務時 handle 跟 handle_path 差在哪、為什麼目標要寫 127.0.0.1 不能寫 localhost、X-Forwarded-Proto 沒被信任會怎麼壞。
發布於 2026-09-06・約 5 分鐘
我用一台反向代理把好幾個服務掛在同一個網址的不同子路徑底下:/stock/、/threads/、/gather/。設定看起來很單純,實際上這三個位置我各踩了一次。
/stock/
/threads/
/gather/
三個症狀長得很像——「首頁打得開,但是壞的」——但成因完全不同。
Caddy 有兩個看起來可以互換的指令:
handle /stock/* { reverse_proxy 127.0.0.1:8001 } handle_path /stock/* { reverse_proxy 127.0.0.1:8001 }
差別是 handle_path 會把匹配到的前綴剝掉再轉發,handle 原樣轉發。
handle_path
handle
我的後端是 uvicorn,而且已經設了 ROOT_PATH=/stock。Starlette 收到請求會自己把這個前綴剝掉再做路由比對。所以用 handle_path 等於剝兩次。
ROOT_PATH=/stock
症狀很有意思:首頁還打得開。 因為 /stock/ 剝掉一次變成 /,再剝一次還是 /,剛好對得上根路由。但是 /stock/static/app.css 剝兩次會變成一個不存在的路徑,回 404。
/
/stock/static/app.css
於是你看到的是一個結構完整、內容正確、但完全沒有 CSS 的頁面。這時很容易往靜態檔案的設定去查,因為看起來就是靜態檔案的問題。
判斷方法:前綴只能剝一次,決定誰來剝。 後端有 root path 設定就讓後端剝,代理用 handle;後端不知道自己被掛在子路徑底下,就讓代理剝,用 handle_path。兩邊都做等於沒做。
Nginx 的對應寫法是 proxy_pass 後面有沒有帶結尾斜線:proxy_pass http://127.0.0.1:8001; 不剝,proxy_pass http://127.0.0.1:8001/; 剝。這個一個字元的差別造成完全不同的行為,是 Nginx 最有名的坑之一。
proxy_pass
proxy_pass http://127.0.0.1:8001;
proxy_pass http://127.0.0.1:8001/;
reverse_proxy localhost:8001 # ❌ reverse_proxy 127.0.0.1:8001 # ✅
容器的 /etc/hosts 通常把 localhost 同時指到 127.0.0.1 和 ::1。而 uvicorn 預設只綁 IPv4。解析到 ::1 就是 connection refused。
/etc/hosts
localhost
127.0.0.1
::1
connection refused
會不會中要看解析器先回哪個位址,所以症狀可能是「有時候通有時候不通」,那比穩定壞掉更難查。
寫死 IPv4 位址最省事。這條在 Node 對外連線的場景也有一個對稱版本:目標網域有 AAAA 紀錄而你的環境沒有 IPv6 出口時,Node 會卡在 IPv6 上直到 timeout。方向相反,成因是同一個——IPv4 跟 IPv6 兩條路,其中一條是死的,而選路的邏輯不知道。
我這台的 TLS 是由更外層終結的,反向代理自己跑純 HTTP。外層轉進來的時候會帶:
X-Forwarded-Proto: https
Caddy 預設不信任來源送來的 X-Forwarded-*。這是對的預設值:這些標頭是可以偽造的,隨便相信等於讓外面的人決定你的後端以為自己在什麼協定上。
X-Forwarded-*
代價是它會用自己實際收到的 scheme(http)去覆蓋掉那個標頭。後端拿到 http,產生出來的絕對網址就是 http://...,而使用者的頁面是 https。瀏覽器把這些資源當 mixed content 擋掉。
http
http://...
症狀又是「頁面打得開但是壞的」,而且 Console 裡的錯誤訊息是講 mixed content,不會提到代理。
修法是明確宣告哪些來源可信:
{ servers { trusted_proxies static 127.0.0.1/32 ::1/128 } }
只列你真的信任的來源。 這裡列的是 loopback,因為終結 TLS 的那層跟代理在同一個網路命名空間裡。寫成 0.0.0.0/0 就等於把這個安全預設整個關掉。
0.0.0.0/0
Nginx 的對應是 set_real_ip_from 加 real_ip_header,同樣要指定來源網段。
set_real_ip_from
real_ip_header
它們都不會讓服務「掛掉」。首頁都打得開,健康檢查都會過,日誌裡都沒有紅字。壞掉的是靜態資源、是子頁面、是某些協定下的行為。
所以驗證代理設定不能只看首頁回 200。至少要抓一個子路徑的靜態檔案,和一個會產生絕對網址的頁面:
curl -s -o /dev/null -w '%{http_code}\n' https://你的網址/stock/ curl -s -o /dev/null -w '%{http_code}\n' https://你的網址/stock/static/app.css curl -s https://你的網址/stock/ | grep -o 'http://[^"]*' | head
第三條如果印出任何東西,你就中了坑三。
標籤:Caddy、反向代理、Nginx、自架服務、uvicorn