報(bào)錯(cuò)排查:The plain HTTP request was sent to HTTPS port)
1. 問題現(xiàn)場一個(gè)看似簡單卻令人困惑的報(bào)錯(cuò)最近在給一個(gè)內(nèi)部服務(wù)配置Nginx反向代理讓它通過HTTPS對(duì)外提供服務(wù)時(shí)遇到了一個(gè)經(jīng)典的報(bào)錯(cuò)The plain HTTP request was sent to HTTPS port。這個(gè)錯(cuò)誤信息直譯過來就是“一個(gè)明文的HTTP請(qǐng)求被發(fā)送到了HTTPS端口”。乍一看這似乎是個(gè)低級(jí)錯(cuò)誤——客戶端用HTTP協(xié)議去訪問一個(gè)配置了SSL的HTTPS端口通常是443。但實(shí)際情況往往更微妙尤其是在反向代理的場景下你明明在瀏覽器里輸入的是https://yourdomain.comNginx日志里卻固執(zhí)地報(bào)出這個(gè)錯(cuò)讓人一時(shí)摸不著頭腦。這個(gè)問題的核心其實(shí)不在于客戶端直接發(fā)錯(cuò)了協(xié)議而在于請(qǐng)求在到達(dá)你配置的Nginxserver塊之前其協(xié)議特征可能就已經(jīng)“丟失”或“錯(cuò)位”了。對(duì)于運(yùn)維和開發(fā)來說這不僅僅是一個(gè)配置錯(cuò)誤更是一個(gè)理解Nginx請(qǐng)求處理流程、SSL終止位置以及代理行為的好機(jī)會(huì)。如果你也正在被這個(gè)報(bào)錯(cuò)困擾或者想深入理解Nginx在代理HTTPS上游服務(wù)時(shí)的內(nèi)部機(jī)制那么接下來的內(nèi)容會(huì)帶你一步步拆解問題從現(xiàn)象到根因再到多種場景下的解決方案。2. 深入理解報(bào)錯(cuò)Nginx的“協(xié)議感知”與端口監(jiān)聽要解決問題首先得明白Nginx為什么會(huì)發(fā)出這樣的抱怨。這需要我們從Nginx監(jiān)聽端口和處理請(qǐng)求的基本邏輯說起。2.1 SSL/TLS握手與協(xié)議識(shí)別當(dāng)一個(gè)客戶端比如瀏覽器嘗試與服務(wù)器建立HTTPS連接時(shí)會(huì)發(fā)生一個(gè)叫做TLS握手的過程。在這個(gè)握手的最初階段客戶端會(huì)發(fā)送一個(gè)ClientHello消息這個(gè)消息本身是明文的但它包含了一個(gè)關(guān)鍵信息它打算使用TLS協(xié)議。服務(wù)器在收到這個(gè)ClientHello后才會(huì)開始進(jìn)行密鑰交換等后續(xù)加密步驟。Nginx的listen指令在配置了ssl參數(shù)后例如listen 443 ssl;它就會(huì)在指定的端口這里是443上期待這種TLS握手的發(fā)生。它會(huì)在TCP連接建立后立即嘗試讀取并解析ClientHello。如果它收到的第一個(gè)數(shù)據(jù)包不符合TLS握手的格式Nginx就會(huì)認(rèn)為這是一個(gè)普通的、未加密的HTTP請(qǐng)求于是拋出了The plain HTTP request was sent to HTTPS port這個(gè)錯(cuò)誤。2.2 反向代理場景下的復(fù)雜性在簡單的靜態(tài)網(wǎng)站服務(wù)中這個(gè)錯(cuò)誤通常意味著客戶端真的用http://訪問了https://的地址。但在反向代理場景下情況就復(fù)雜了。你的Nginx可能同時(shí)監(jiān)聽80和443端口負(fù)責(zé)將請(qǐng)求轉(zhuǎn)發(fā)給后端的應(yīng)用服務(wù)器比如運(yùn)行在8080端口的Tomcat或者另一個(gè)HTTP服務(wù)。這里的關(guān)鍵在于代理鏈。你的Nginx作為邊緣服務(wù)器終止了來自客戶端的HTTPS連接即解密了數(shù)據(jù)。然后它需要?jiǎng)?chuàng)建一個(gè)新的請(qǐng)求發(fā)送給后端服務(wù)器。這個(gè)新請(qǐng)求使用什么協(xié)議完全由Nginx的proxy_pass指令所在location塊的配置決定與客戶端最初的協(xié)議無關(guān)。最常見的錯(cuò)誤配置模式是這樣的server { listen 443 ssl; server_name example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { # 錯(cuò)誤配置直接使用http://指向后端 proxy_pass http://backend_server:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 這個(gè)頭很重要 } }這個(gè)配置看起來沒問題Nginx確實(shí)在443端口終止了SSL。但是如果后端服務(wù)器backend_server:8080自己也配置了SSL期待一個(gè)HTTPS請(qǐng)求那么問題就來了。Nginx使用http://協(xié)議發(fā)過去一個(gè)明文HTTP請(qǐng)求后端服務(wù)器如果在8080端口也期待TLS握手就會(huì)拒絕這個(gè)請(qǐng)求。然而這個(gè)拒絕信息在傳遞回Nginx時(shí)可能被轉(zhuǎn)換或丟失最終在Nginx的錯(cuò)誤日志中呈現(xiàn)為開頭的那個(gè)報(bào)錯(cuò)因?yàn)樗l(fā)生在Nginx自己的443端口監(jiān)聽邏輯里。另一種情況是你可能在同一個(gè)server塊里混合了帶ssl和不帶ssl的listen指令或者配置了錯(cuò)誤的default_server導(dǎo)致流量被錯(cuò)誤的服務(wù)器塊處理。3. 核心排查鏈路從日志到配置的逐層驗(yàn)證當(dāng)遇到這個(gè)報(bào)錯(cuò)時(shí)不要急于修改配置先按照一個(gè)清晰的排查鏈路來定位問題。盲目修改往往會(huì)讓問題更復(fù)雜。3.1 第一步檢查Nginx錯(cuò)誤日志與訪問日志日志是定位問題的第一現(xiàn)場。你需要同時(shí)查看錯(cuò)誤日志error_log和訪問日志access_log并且確保日志級(jí)別足夠詳細(xì)例如error_log /var/log/nginx/error.log debug;在排查時(shí)臨時(shí)開啟debug級(jí)別事后記得改回。在錯(cuò)誤日志中找到報(bào)錯(cuò)The plain HTTP request was sent to HTTPS port的那一行。注意看它前面的連接標(biāo)識(shí)如client: 192.168.1.100和時(shí)間戳。在訪問日志中根據(jù)時(shí)間戳和客戶端IP找到對(duì)應(yīng)的訪問記錄。重點(diǎn)看幾個(gè)字段$request 記錄的是完整的請(qǐng)求行例如GET /api/data HTTP/1.1。這里顯示的是Nginx最終處理請(qǐng)求時(shí)認(rèn)定的協(xié)議。如果這里顯示HTTP/1.1而不是HTTPS那說明在Nginx看來這個(gè)請(qǐng)求就是HTTP。$scheme 這個(gè)變量代表請(qǐng)求使用的協(xié)議http或https。在proxy_set_header中我們常用$scheme來告訴后端請(qǐng)求最初的協(xié)議。但在訪問日志里它反映的是Nginx處理時(shí)的協(xié)議判斷。$ssl_protocol 如果這個(gè)字段是空的那就證實(shí)了Nginx沒有在這個(gè)連接上檢測到SSL握手。注意臨時(shí)修改日志級(jí)別和格式可以獲取更多信息。你可以在http塊或server塊中自定義一個(gè)日志格式包含更多變量例如log_format debug_log $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $scheme $ssl_protocol $server_port; access_log /var/log/nginx/debug_access.log debug_log;3.2 第二步驗(yàn)證Nginx配置語法與加載在修改任何配置之前先用nginx -t命令測試配置文件的語法是否正確。這個(gè)命令會(huì)檢查語法并告訴你配置文件路徑。確保你修改的是Nginx真正加載的那個(gè)配置文件。有時(shí)候問題可能出在配置片段include的文件或者多個(gè)配置文件沖突上。使用nginx -T可以打印出Nginx實(shí)際加載的所有配置方便你全局搜索listen、ssl和proxy_pass指令。3.3 第三步分析完整的請(qǐng)求路徑根據(jù)日志畫出請(qǐng)求的完整路徑客戶端 - (HTTPS) - Nginx 443端口。Nginx 解密 - 根據(jù)server_name和location匹配決定轉(zhuǎn)發(fā)。Nginx - (???) - 后端服務(wù)器。你需要明確第3步中Nginx到底用了什么協(xié)議、什么端口去連接后端。使用proxy_pass http://backend:port就是HTTP使用proxy_pass https://backend:port就是HTTPS。這里的一個(gè)微小差別就是問題的根源。3.4 第四步檢查后端服務(wù)狀態(tài)與期望如果懷疑是后端服務(wù)的問題直接繞過Nginx測試后端。如果后端服務(wù)監(jiān)聽8080你可以用curl命令測試# 測試后端是否響應(yīng)HTTP curl -v http://backend_server_ip:8080/health # 如果后端期待HTTPS嘗試假設(shè)后端有自簽名證書 curl -vk https://backend_server_ip:8443/health通過curl的詳細(xì)輸出(-v)你可以看到完整的HTTP請(qǐng)求和響應(yīng)頭以及SSL握手情況。如果后端只接受HTTPS而你用HTTP去訪問后端通常會(huì)返回一個(gè)400 Bad Request或者直接關(guān)閉連接。4. 解決方案大全針對(duì)不同場景的修復(fù)策略找到了問題根源解決方案就清晰了。以下是針對(duì)不同場景的配置修正方法。4.1 場景一Nginx代理HTTP后端但客戶端誤訪問這是最單純的情況。你的Nginx配置了SSL代理到一個(gè)HTTP后端但用戶或者某個(gè)爬蟲直接用http://訪問了你的443端口。解決方案在監(jiān)聽443端口的server塊中配置一個(gè)重定向?qū)⑺蠬TTP請(qǐng)求重定向到HTTPS。但注意對(duì)于已經(jīng)到達(dá)443端口的明文HTTP請(qǐng)求Nginx會(huì)先報(bào)錯(cuò)然后才能處理重定向指令。因此更常見的做法是在監(jiān)聽80端口的server塊中做重定向。# 監(jiān)聽80端口的server塊處理所有HTTP請(qǐng)求 server { listen 80; server_name example.com www.example.com; # 永久重定向到HTTPS return 301 https://$server_name$request_uri; } # 監(jiān)聽443端口的server塊處理所有HTTPS請(qǐng)求 server { listen 443 ssl; server_name example.com www.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { proxy_pass http://backend_server:8080; # 代理到HTTP后端 ... # 其他proxy_set_header配置 } }4.2 場景二Nginx需要代理到HTTPS后端上游服務(wù)自帶SSL這是導(dǎo)致開頭報(bào)錯(cuò)的最常見、也最隱蔽的場景。你的后端服務(wù)例如一個(gè)Java應(yīng)用使用Spring Boot內(nèi)置的HTTPS或者另一個(gè)Nginx自己就提供了HTTPS端點(diǎn)。錯(cuò)誤配置proxy_pass http://secure-backend:8443;正確配置proxy_pass https://secure-backend:8443;僅僅是把http://改成https://嗎還不夠。當(dāng)你使用proxy_pass https://...時(shí)Nginx需要與后端建立一個(gè)新的HTTPS連接這意味著它需要驗(yàn)證后端服務(wù)器的證書。location / { proxy_pass https://secure-backend:8443; # 關(guān)鍵的頭信息傳遞 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 告訴后端最初的協(xié)議是https # HTTPS代理特有的配置 proxy_ssl_verify on; # 驗(yàn)證后端證書生產(chǎn)環(huán)境建議開啟 proxy_ssl_verify_depth 2; proxy_ssl_trusted_certificate /path/to/trusted_ca_certs.pem; # 信任的CA證書 proxy_ssl_certificate /path/to/client_cert.pem; # 如果后端需要客戶端證書 proxy_ssl_certificate_key /path/to/client_cert.key; proxy_ssl_name $proxy_host; # 用于SNI通常設(shè)為$proxy_host proxy_ssl_server_name on; # 啟用SNI proxy_ssl_protocols TLSv1.2 TLSv1.3; # 指定協(xié)議版本 proxy_ssl_ciphers HIGH:!aNULL:!MD5; # 指定加密套件 }實(shí)操心得在內(nèi)網(wǎng)環(huán)境中后端可能使用自簽名證書。這時(shí)你需要將后端證書的CA或證書本身添加到proxy_ssl_trusted_certificate并將proxy_ssl_verify設(shè)置為off僅限測試環(huán)境。否則Nginx會(huì)因證書驗(yàn)證失敗而無法連接到后端錯(cuò)誤日志中會(huì)出現(xiàn)SSL_do_handshake() failed等相關(guān)錯(cuò)誤這可能與最初的報(bào)錯(cuò)不同但根本原因相關(guān)。4.3 場景三混合監(jiān)聽與默認(rèn)服務(wù)器沖突如果你的Nginx配置了多個(gè)server塊并且使用了default_server參數(shù)或者80和443端口的配置不匹配可能導(dǎo)致流量被錯(cuò)誤的server塊處理。# 錯(cuò)誤示例模糊的默認(rèn)服務(wù)器 server { listen 80 default_server; listen 443 ssl default_server; # 443也設(shè)置了default_server server_name _; # 這個(gè)塊可能會(huì)捕獲所有未知域名的443請(qǐng)求如果沒配ssl就會(huì)報(bào)錯(cuò) return 444; # 或者一些其他處理 } server { listen 443 ssl; server_name example.com; # 正確的配置 }解決方案明確每個(gè)server塊的server_name謹(jǐn)慎使用default_server。確保監(jiān)聽443端口的server塊都正確配置了ssl參數(shù)和證書。對(duì)于不需要處理HTTPS的default_server只監(jiān)聽80端口。4.4 場景四使用stream模塊進(jìn)行TCP/UDP代理如果你使用Nginx的stream模塊進(jìn)行四層代理例如代理數(shù)據(jù)庫端口或某些非HTTP協(xié)議那么stream塊內(nèi)的配置不涉及HTTP/HTTPS協(xié)議也就不會(huì)出現(xiàn)這個(gè)錯(cuò)誤。這個(gè)錯(cuò)誤是http模塊特有的。確保你沒有錯(cuò)誤地將HTTP代理的配置proxy_pass http://...放在了本應(yīng)使用四層代理的地方。5. 進(jìn)階排查與相關(guān)陷阱即使按照上述方案修改了問題可能依然存在或者以其他形式出現(xiàn)。這里有幾個(gè)更深層次的排查點(diǎn)和常見陷阱。5.1 檢查防火墻與負(fù)載均衡器在企業(yè)網(wǎng)絡(luò)中Nginx前面可能還有一層負(fù)載均衡器如F5, AWS ALB/NLB或防火墻。這些設(shè)備可能會(huì)進(jìn)行SSL卸載Termination然后將解密后的HTTP流量轉(zhuǎn)發(fā)給后端的Nginx。如果它們配置錯(cuò)誤比如將HTTPS流量解密后卻仍然用TCP模式轉(zhuǎn)發(fā)到Nginx的443端口那么Nginx在443端口收到的就是明文HTTP流量從而觸發(fā)報(bào)錯(cuò)。如何排查查看Nginx訪問日志中的$remote_addr。如果這個(gè)IP不是你客戶端的公網(wǎng)IP而是某個(gè)內(nèi)網(wǎng)IP如10.x.x.x, 172.x.x.x那么流量很可能經(jīng)過了中間設(shè)備。你需要聯(lián)系網(wǎng)絡(luò)團(tuán)隊(duì)確認(rèn)負(fù)載均衡器的監(jiān)聽器Listener配置是否正確確保它要么將HTTPS流量透傳TCP Passthrough到Nginx要么在SSL卸載后將流量轉(zhuǎn)發(fā)到Nginx的80端口或其他非SSL端口。5.2 HTTP/2與協(xié)議升級(jí)現(xiàn)代瀏覽器和Nginx都支持HTTP/2 over HTTPS (h2)。雖然這通常不會(huì)直接導(dǎo)致該錯(cuò)誤但在一些邊緣情況下如果客戶端嘗試在明文HTTP連接上發(fā)起HTTP/2連接或者配置混亂也可能引發(fā)問題。確保你的SSL配置支持現(xiàn)代協(xié)議并且沒有錯(cuò)誤地配置了http2指令在非SSL的listen上。5.3 代理頭信息傳遞的重要性頭信息X-Forwarded-Proto對(duì)于后端應(yīng)用至關(guān)重要。許多Web框架如Spring Boot, Django, Express依賴這個(gè)頭來判斷原始請(qǐng)求是否通過HTTPS訪問從而正確地生成重定向URL或設(shè)置安全cookie。如果這個(gè)頭傳遞錯(cuò)誤比如傳成了http即使前端是HTTPS后端也可能錯(cuò)誤地生成一個(gè)HTTP的URL導(dǎo)致客戶端又去發(fā)起HTTP請(qǐng)求形成循環(huán)或錯(cuò)誤。在你的location塊中確保設(shè)置了proxy_set_header X-Forwarded-Proto $scheme;并且在后端應(yīng)用中配置為信任這個(gè)頭例如Spring Boot的server.forward-headers-strategynative或使用X-Forwarded-Proto過濾器。5.4 Docker與容器網(wǎng)絡(luò)中的特殊問題在Docker環(huán)境中運(yùn)行Nginx時(shí)網(wǎng)絡(luò)拓?fù)渥兊酶訌?fù)雜。一個(gè)常見的錯(cuò)誤是在Docker Compose中Nginx容器通過服務(wù)名如app:8080代理到應(yīng)用容器但應(yīng)用容器內(nèi)部只暴露了HTTP端口。然而如果你在Nginx配置中錯(cuò)誤地將服務(wù)名映射到了一個(gè)外部定義的、帶HTTPS的域名上就會(huì)出問題。確保你的Docker網(wǎng)絡(luò)內(nèi)通信使用正確的協(xié)議和端口。通常容器間通信使用HTTP即可SSL在邊緣的Nginx容器終止。檢查Nginx容器中proxy_pass指令指向的地址和端口是否確實(shí)是后端應(yīng)用容器暴露的端口。6. 一個(gè)完整的配置示例與調(diào)試流程讓我們通過一個(gè)完整的例子串聯(lián)起配置、測試和調(diào)試的全過程。目標(biāo)將域名api.example.com的HTTPS流量通過Nginx反向代理到內(nèi)網(wǎng)一個(gè)運(yùn)行在https://192.168.1.10:9443上的Spring Boot應(yīng)用自帶SSL使用自簽名證書。步驟1準(zhǔn)備證書將Spring Boot應(yīng)用的自簽名證書或其CA證書拷貝到Nginx服務(wù)器上例如/etc/nginx/ssl/backend-ca.crt。步驟2編寫Nginx配置# /etc/nginx/conf.d/api-proxy.conf upstream backend_https { # 如果后端是集群可以在這里定義多個(gè)server server 192.168.1.10:9443; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.example.com; # 邊緣Nginx自己的SSL證書由公共CA簽發(fā) ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; # 強(qiáng)化SSL配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512; ssl_prefer_server_ciphers off; location / { # 關(guān)鍵使用https://協(xié)議連接后端 proxy_pass https://backend_https; # 傳遞必要的頭信息 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 傳遞原始協(xié)議為https # 配置與后端HTTPS連接的參數(shù) proxy_ssl_verify off; # 因?yàn)楹蠖耸亲院灻C書臨時(shí)關(guān)閉驗(yàn)證生產(chǎn)環(huán)境應(yīng)配置信任證書 # proxy_ssl_trusted_certificate /etc/nginx/ssl/backend-ca.crt; # proxy_ssl_verify on; proxy_ssl_name $proxy_host; proxy_ssl_server_name on; # 超時(shí)設(shè)置 proxy_connect_timeout 75s; proxy_send_timeout 3600s; proxy_read_timeout 3600s; } } # HTTP重定向 server { listen 80; listen [::]:80; server_name api.example.com; return 301 https://$server_name$request_uri; }步驟3測試與調(diào)試語法檢查sudo nginx -t重載配置sudo nginx -s reload從外部測試curl -v https://api.example.com/actuator/health查看Nginx日志tail -f /var/log/nginx/error.logtail -f /var/log/nginx/access.log(使用包含$scheme和$ssl_protocol的日志格式)直接測試后端在Nginx服務(wù)器上curl -vk https://192.168.1.10:9443/actuator/health確認(rèn)后端服務(wù)本身是可用的。檢查連接如果還有問題可以在Nginx服務(wù)器上用tcpdump抓包分析Nginx與后端服務(wù)器192.168.1.10:9443之間的通信看TCP連接是否建立是否有TLS握手。通過這樣系統(tǒng)性的配置和排查The plain HTTP request was sent to HTTPS port這個(gè)報(bào)錯(cuò)就不再是一個(gè)黑盒錯(cuò)誤而是指引你深入理解網(wǎng)絡(luò)協(xié)議棧和Nginx配置的清晰路標(biāo)。記住關(guān)鍵在于理清整個(gè)數(shù)據(jù)流中每一個(gè)環(huán)節(jié)對(duì)協(xié)議的期望和處理方式。