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