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