ทำไม Nginx proxy WebSocket ไม่ได้ และวิธีแก้
หน้าเว็บโหลดได้แต่แอปค้างที่ connecting เพราะ nginx ส่ง HTTP/1.0 ขึ้น upstream และตัด header Upgrade ทิ้ง คู่มือนี้ให้บล็อก config ที่ใช้ได้จริง พร้อมวิธีตรวจทีละชั้น
WebSocket ไม่ผ่าน nginx เพราะอะไร
nginx reverse proxy ที่ตั้งค่าแบบพื้นฐานคุยกับ upstream ด้วย HTTP/1.0 และไม่ส่ง header Upgrade กับ Connection ต่อขึ้นไปให้แอป การ handshake ของ WebSocket จึงไม่เกิดขึ้นเลย ผลคือหน้า HTML และไฟล์ static โหลดได้ตามปกติ เพราะส่วนนั้นเป็น HTTP ธรรมดา แต่ส่วนที่ต้องอัปเดตแบบ real time จะค้างอยู่ที่คำว่า connecting หรือวน reconnect ไม่จบ
ทางแก้อยู่ที่สามบรรทัดใน location เดียวกับ proxy_pass คือ proxy_http_version 1.1 และ proxy_set_header อีกสองตัว โดยมี map หนึ่งบล็อกที่ระดับ http คอยจ่ายค่าให้ header ตัวที่สอง ส่วนอาการอีกแบบหนึ่ง คือเชื่อมต่อติดแล้วหลุดเป็นรอบ ๆ ทุกประมาณหนึ่งนาที นั่นคนละสาเหตุกัน ตัวการคือ proxy_read_timeout ซึ่งมีค่าเริ่มต้น 60 วินาที
บทความนี้เขียนบน Ubuntu 24.04 กับ nginx จาก apt คำสั่งทั้งหมดใช้ path ของแพ็กเกจ nginx บน Debian และ Ubuntu ถ้าคุณยังไม่เคยแยกส่วนประกอบของบล็อก proxy มาก่อน คำอธิบาย config ของ nginx reverse proxy ทีละบรรทัด จะช่วยให้อ่านตัวอย่างข้างล่างได้เร็วขึ้น
WebSocket เริ่มต้นจากคำขอ HTTP ธรรมดา
WebSocket (ช่องทางสื่อสารสองทางบน TCP เส้นเดียว) ไม่ได้เปิดพอร์ตใหม่ของตัวเอง มันเริ่มจากคำขอ HTTP ปกติที่เบราว์เซอร์ส่งไปพร้อม header กลุ่มหนึ่ง ได้แก่ Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key และ Sec-WebSocket-Version เซิร์ฟเวอร์ที่รับได้จะตอบกลับด้วยการเปลี่ยนโปรโตคอล จากนั้น TCP เส้นเดิมเลิกเป็น HTTP แล้วกลายเป็นท่อส่ง frame สองทางจนกว่าฝ่ายใดฝ่ายหนึ่งจะปิด
เมื่อมี reverse proxy คั่นกลาง proxy ต้องทำสองอย่างให้ครบ อย่างแรกคือส่ง handshake ขึ้นไปให้แอปโดยไม่ทำ header หาย อย่างที่สองคือปล่อยให้การเชื่อมต่อนั้นอยู่ยาว ไม่ไปตัดทิ้งตอนที่ไม่มีข้อมูลวิ่ง nginx ไม่ทำทั้งสองอย่างนี้ให้เองโดยอัตโนมัติ และนั่นคือที่มาของทั้งบทความ
header ชุด Sec-WebSocket-* ไม่ใช่ปัญหา มันเป็น header แบบ end to end ที่ proxy ส่งต่อให้อยู่แล้ว ปัญหาอยู่ที่ Upgrade กับ Connection เท่านั้น
สาเหตุที่หนึ่ง proxy_http_version มีค่าเริ่มต้นเป็น 1.0
nginx ใช้ HTTP/1.0 เวลาคุยกับ upstream เว้นแต่คุณสั่งเป็นอย่างอื่น นี่เป็นค่าเริ่มต้นของ directive proxy_http_version กลไก Upgrade ที่ WebSocket ใช้ถูกนิยามไว้บน HTTP/1.1 (RFC 6455) ดังนั้นต่อให้คุณส่ง header ครบทุกตัว แอปฝั่ง upstream ที่ทำตามมาตรฐานก็มีสิทธิ์ปฏิเสธ เพราะคำขอที่มันได้รับเป็น HTTP/1.0
บรรทัดที่แก้เรื่องนี้คือ proxy_http_version 1.1; และมันต้องอยู่ในบล็อก location เดียวกับ proxy_pass ที่รับคำขอ WebSocket จริง ๆ
สาเหตุที่สอง Upgrade และ Connection เป็น hop by hop header
มาตรฐาน HTTP แบ่ง header ออกเป็นสองกลุ่ม กลุ่ม end to end มีไว้ให้ปลายทางอ่าน proxy ต้องส่งต่อ ส่วนกลุ่ม hop by hop มีความหมายเฉพาะกับการเชื่อมต่อช่วงนั้นช่วงเดียว proxy ต้องไม่ส่งต่อ Connection และ header ที่ Connection ระบุชื่อไว้ ซึ่งในกรณีนี้คือ Upgrade อยู่ในกลุ่มหลัง
nginx ทำตามกฎนี้อย่างเคร่งครัด มันจึงกิน header สองตัวนั้นไว้เอง แอปที่อยู่ข้างหลังได้รับคำขอที่ไม่มีอะไรบอกว่านี่คือ WebSocket มันจึงตอบเป็นหน้าเว็บธรรมดาหรือปฏิเสธไป เบราว์เซอร์ที่ไม่ได้รับการเปลี่ยนโปรโตคอลจะถือว่าการเชื่อมต่อล้มเหลว แล้วโค้ดฝั่ง client ของแอปส่วนใหญ่จะพยายามใหม่เรื่อย ๆ ซึ่งคืออาการวน reconnect ที่คุณเห็น
การแก้คือประกาศ header สองตัวนี้ขึ้นมาใหม่ด้วยมือ นั่นคือสิ่งที่ proxy_set_header Upgrade กับ proxy_set_header Connection ทำ
ทำไมต้องมี map ไม่ตั้ง Connection เป็น upgrade ไปเลย
คุณเขียน proxy_set_header Connection "upgrade"; ตรง ๆ ได้ และมันจะใช้งานได้กับ WebSocket แต่ location เดียวกันนั้นมักรับคำขอ HTTP ธรรมดาด้วย เช่น หน้า UI, ไฟล์ static และ REST API การบังคับให้ทุกคำขอมี Connection: upgrade แปลว่าคุณกำลังบอกแอปว่าคำขอที่ไม่เกี่ยวกับ WebSocket เลยก็ขอเปลี่ยนโปรโตคอลเหมือนกัน ผลที่ตามมาแตกต่างกันไปตามแอป บางตัวไม่สนใจ บางตัวตอบผิด และ keepalive ระหว่าง nginx กับ upstream ใช้ไม่ได้ เพราะ Connection ถูกเขียนทับไปแล้ว
map แก้เรื่องนี้ด้วยการทำให้ค่าของ header ขึ้นกับคำขอแต่ละอัน ถ้าคำขอนั้นมี Upgrade เข้ามา ตัวแปรจะมีค่า upgrade ถ้าไม่มี ตัวแปรจะมีค่า close คำขอปกติจึงเดินทางแบบปกติ ส่วนคำขอ WebSocket ได้ header ที่มันต้องการ
map ประกาศได้เฉพาะที่ระดับ http ไม่ใช่ใน server หรือ location ถ้าวางผิดที่ nginx จะไม่ยอมโหลด config เลย
กับดักที่คนติดบ่อย proxy_set_header ไม่สืบทอดข้ามระดับ
กฎข้อนี้ทำให้ config ที่ดูถูกทุกอย่างพังได้เงียบ ๆ proxy_set_header สืบทอดจากระดับนอกมาให้ระดับใน ก็ต่อเมื่อระดับในนั้นไม่มี proxy_set_header ของตัวเองแม้แต่บรรทัดเดียว พอคุณเขียน header ของ WebSocket ไว้ที่ระดับ server แล้วใน location มี proxy_set_header Host $host; อยู่หนึ่งบรรทัด header ทั้งชุดจากระดับ server จะหายไปทันที
วิธีที่ปลอดภัยคือเขียน proxy_set_header ทุกตัวที่ location นั้นต้องใช้ ไว้ใน location นั้นครบชุด อย่ากระจายคนละระดับ
บล็อก config ที่ใช้ได้จริง
ติดตั้ง nginx ก่อน ถ้ายังไม่มี
sudo apt update
sudo apt install -y nginxไฟล์แรกคือ map วางไว้ใน /etc/nginx/conf.d/ เพราะ nginx.conf ของแพ็กเกจนี้ include โฟลเดอร์ดังกล่าวไว้ข้างในบล็อก http อยู่แล้ว การวางตรงนี้จึงได้ระดับที่ถูกต้องโดยไม่ต้องแก้ไฟล์หลัก
# /etc/nginx/conf.d/websocket-upgrade.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}ไฟล์ที่สองคือ server block ตัวอย่างนี้ proxy ไปที่แอปซึ่งฟังอยู่ที่ 127.0.0.1:5678 เปลี่ยนพอร์ตกับชื่อโดเมนให้ตรงกับของคุณ ส่วน path ของใบรับรองมาจาก certbot ถ้าคุณยังไม่ได้ออกใบรับรอง ให้ทำ ขั้นตอนขอใบรับรอง Let's Encrypt ด้วย certbot บน Ubuntu 24.04 ให้เสร็จก่อน
# /etc/nginx/sites-available/app.conf
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}เปิดใช้งานไฟล์นี้ด้วย symlink
sudo ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/app.confheader สามตัวล่างไม่ได้เกี่ยวกับ handshake โดยตรง แต่เกี่ยวกับสิ่งที่แอปจะบอกเบราว์เซอร์ต่อ Host ทำให้แอปรู้ชื่อโดเมนจริง เพราะโดยค่าเริ่มต้น nginx จะส่ง Host เป็นค่าที่เขียนไว้ใน proxy_pass ส่วน X-Forwarded-Proto สำคัญมากกับ WebSocket แอปหลายตัวประกอบ URL ของ socket จากค่านี้ ถ้ามันคิดว่าคำขอเข้ามาแบบ http มันจะสั่งให้เบราว์เซอร์ต่อไปที่ ws:// ทั้งที่หน้าเว็บเป็น https แล้วเบราว์เซอร์จะบล็อกการต่อนั้นเองในฐานะ mixed content โดยที่ nginx ไม่ได้ทำอะไรผิดเลย
proxy_read_timeout ตั้งเท่าไร และแลกอะไรไป
proxy_read_timeout คือเวลาที่ nginx ยอมรอข้อมูลจาก upstream นับใหม่ทุกครั้งที่มีข้อมูลเข้ามา ค่าเริ่มต้นคือ 60 วินาที WebSocket ที่ไม่มีอะไรวิ่งนานกว่านั้นจะโดนตัด แล้ว client ก็ต่อใหม่ วนแบบนี้ไปเรื่อย ๆ ถ้าแอปของคุณหลุดเป็นจังหวะสม่ำเสมอราวหนึ่งนาที นี่คือคำตอบ
การตั้ง proxy_read_timeout 3600s; ซื้อความนิ่งมาด้วยราคาสองอย่าง อย่างแรกคือ connection ที่ตายไปแล้วจริง ๆ จะค้างอยู่ในตาราง connection ของ nginx นานถึงหนึ่งชั่วโมงก่อนจะถูกเก็บกวาด อย่างที่สองคือ location นี้รับ HTTP ธรรมดาด้วย คำขอปกติที่ upstream ค้างจะรอนานตามไปด้วย แทนที่จะล้มเร็ว
ทางที่ดีกว่าคือให้แอปส่ง ping frame เองเป็นระยะ แอปที่ทำแบบนั้นจะรักษาการเชื่อมต่อไว้ได้ด้วย timeout ระดับสองถึงสามนาที และคุณไม่ต้องยืดเป็นชั่วโมง ถ้าแอปไม่มีตัวเลือกนั้น อีกทางคือแยก location ของ path ที่เป็น WebSocket ออกมา แล้วตั้ง timeout ยาวเฉพาะตรงนั้น ส่วน location หลักปล่อยไว้ตามค่าเริ่มต้น
ตรวจ config ก่อน reload
สองคำสั่งนี้ทำงานคนละหน้าที่กัน อย่ารันแค่อันเดียว
sudo nginx -tคำสั่งนี้อ่าน config ทั้งชุดแล้วรายงานว่ามันยอมรับหรือไม่ อ่านสิ่งที่มันพิมพ์ออกมาให้จบ ถ้ามันบ่งชี้ปัญหา ให้ดูว่ามันชี้ไปที่ไฟล์ไหนบรรทัดไหน ความผิดที่พบบ่อยที่สุดคือ map ไปอยู่ผิดระดับ กับตัวแปร $connection_upgrade ที่ถูกอ้างถึงทั้งที่ยังไม่มีใครประกาศ
sudo nginx -T | grep -n 'connection_upgrade'nginx -T พิมพ์ config ที่รวมทุก include เข้าด้วยกันแล้ว นี่คือสิ่งที่ nginx เห็นจริง ๆ ไม่ใช่สิ่งที่คุณคิดว่าคุณเขียนไว้ ดูผลลัพธ์ว่ามีทั้งบรรทัดที่ประกาศ map และบรรทัดที่ใช้งานมันใน location หรือไม่ ถ้าเจอแค่อย่างใดอย่างหนึ่ง แปลว่าไฟล์ของคุณยังไม่ถูก include เข้ามา
sudo nginx -T | grep -n -A 25 'server_name app.example.com'อ่านบล็อกที่ได้ แล้วเทียบกับตัวอย่างข้างบนทีละบรรทัด สิ่งที่ต้องยืนยันคือ proxy_http_version, proxy_set_header ทั้งสองตัวของ WebSocket และ proxy_read_timeout อยู่ใน location เดียวกับ proxy_pass ที่รับ path ของ WebSocket จริง ไม่ใช่อยู่คนละ location
เมื่อทั้งสองคำสั่งให้ผลที่คุณพอใจแล้วจึงโหลดใหม่
sudo systemctl reload nginxreload ทำให้ worker ชุดใหม่ใช้ config ใหม่ ส่วน worker เก่ายังถือ connection เดิมไว้จนกว่าจะปิด นั่นแปลว่า WebSocket ที่เปิดอยู่ก่อนหน้านี้ยังใช้ config เก่าต่อไป ตอนทดสอบให้ปิดแท็บเบราว์เซอร์แล้วเปิดใหม่ทุกครั้ง ไม่อย่างนั้นคุณจะกำลังวัดผลของ config เก่าอยู่
ตรวจจากฝั่งผู้ใช้ว่าผ่านจริงหรือไม่
ขั้นตอนต่อไปนี้ต้องรันกับแอปจริงของคุณ เปิดหน้าเว็บของแอป กด F12 เพื่อเปิด developer tools ไปที่แท็บ Network แล้วกรองด้วยตัวกรอง WS จากนั้นรีเฟรชหน้า คุณจะเห็นรายการคำขอที่เป็น WebSocket ขึ้นมา คลิกเข้าไปดูรายละเอียดของคำขอนั้น แล้วดูว่ามันรายงานสถานะอะไร และมีแท็บที่แสดง frame วิ่งเข้าออกหรือไม่
ถ้าคำขอนั้นไม่ได้เปลี่ยนไปเป็นการเชื่อมต่อแบบ WebSocket directive ที่ต้องสงสัยเรียงตามลำดับคือ proxy_http_version แล้วตามด้วย proxy_set_header Upgrade และ proxy_set_header Connection ทั้งสามตัวนี้ต้องอยู่ใน location ที่รับ path นั้น ถ้ามันเปลี่ยนสำเร็จแล้วมี frame วิ่ง แต่รายการหายไปเป็นจังหวะ ให้ไปดู proxy_read_timeout
ถ้าอยากทดสอบจากเชลล์โดยไม่ผ่านเบราว์เซอร์ ให้ยิง handshake ด้วยมือ แก้โดเมนและ path ให้ตรงกับของแอปคุณ
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" \
"https://app.example.com/ws"อ่านสิ่งที่ curl รายงานกลับมา แล้วถามตัวเองว่านี่คือการเปลี่ยนโปรโตคอลหรือเป็นการตอบแบบหน้าเว็บทั่วไป ถ้าเป็นอย่างหลัง ให้รันคำสั่งเดิมซ้ำแต่ยิงตรงไปที่แอปบนเครื่อง โดยเปลี่ยน URL เป็น http://127.0.0.1:5678/ws ผลที่ต่างกันของสองรอบนี้แยกปัญหาออกเป็นสองฝั่งได้ทันที ถ้ายิงตรงแล้วได้ผลที่ถูกแต่ยิงผ่าน nginx แล้วไม่ได้ ปัญหาอยู่ที่ config ของ nginx ถ้ายิงตรงก็ยังไม่ได้ nginx ไม่เกี่ยวเลย ปัญหาอยู่ที่แอปหรือการตั้งค่าของแอป
ยังไม่ผ่าน ตรวจทีละชั้น
- location ผิดตัว แอปจำนวนมากวาง WebSocket ไว้ที่ path เฉพาะ เช่น
/socket.io/หรือ/wsถ้าคุณมี location แยกสำหรับ path นั้นอยู่แล้ว nginx จะเลือก location ที่ตรงที่สุด ไม่ใช่location /ที่คุณเพิ่ง header ครบ ให้เอาnginx -Tมาไล่ดูว่ามี location ไหนคลุม path นั้นบ้าง proxy_passมี trailing slash ทำให้ path เปลี่ยน เขียนproxy_pass http://127.0.0.1:5678/;ในlocation /ws/แปลว่า nginx ตัด/ws/ออกก่อนส่งขึ้นไป แอปที่คาดว่าจะเห็น path เต็มจะหาปลายทางไม่เจอ ถ้าไม่แน่ใจ ให้ตัด slash ท้ายออกเพื่อส่ง path เดิมขึ้นไปทั้งก้อน- แอปสร้าง URL ผิด protocol ดู
X-Forwarded-Protoและดูว่าแอปมีการตั้งค่า public URL ของตัวเองหรือไม่ แอปอย่าง n8n มีตัวแปรสภาพแวดล้อมสำหรับเรื่องนี้โดยเฉพาะ และ วิธีติดตั้ง n8n บน VPS ด้วย Docker และ HTTPS อธิบายไว้ในบริบทของ compose file ถ้าอาการของคุณคือ n8n ที่ขึ้นสถานะหลุดเป็นช่วง ๆ สาเหตุที่ n8n กลายเป็น offline ซ้ำ ๆ ไล่แยกไว้ทั้งฝั่ง proxy และฝั่งตัวแอป - แอปอยู่ใน Docker และไม่ได้ publish พอร์ตออกมาที่ host
proxy_passไปที่127.0.0.1จะไปไม่ถึงอะไรทั้งนั้น ตรวจด้วยss -ltnpหรือดู วิธีตรวจว่าพอร์ตเปิดอยู่จริงหรือไม่บน Linux ก่อนจะไปแก้ config ต่อ - มีของอีกชั้นอยู่หน้า nginx ถ้ามี CDN, cloud load balancer หรือไฟร์วอลล์ของผู้ให้บริการคั่นอยู่ ของชั้นนั้นมี timeout กับกฎเรื่อง Upgrade ของตัวเอง ทดสอบโดยยิงตรงไปที่ IP ของ VPS เทียบกับยิงผ่านชื่อโดเมน แล้วดูว่าผลต่างกันหรือไม่
- upstream มีหลายเครื่อง WebSocket อยู่ยาวและมักผูกกับ state ในหน่วยความจำของเครื่องที่รับ handshake ถ้าบล็อก
upstreamกระจายโหลดแบบ round robin คำขอ HTTP ของ session เดียวกันอาจไปคนละเครื่อง ให้ใช้ip_hashหรือกลไก sticky ของแอปเอง
ส่วนแอปที่ถอยไปใช้ Server Sent Events หรือ long polling เมื่อ WebSocket ล้มเหลว จะเจออีกอาการหนึ่ง คือข้อความมาถึงเป็นก้อนช้า ๆ แทนที่จะมาทันที กรณีนั้น directive ที่เกี่ยวคือ proxy_buffering ให้ตั้งเป็น off ใน location นั้น เพราะ nginx เก็บ response สะสมไว้ก่อนส่งต่อ ซึ่งขัดกับการสตรีมทีละบรรทัด ข้อควรรู้คือหลัง handshake ของ WebSocket สำเร็จแล้ว nginx จะทำตัวเป็นท่อส่งไบต์ตรง ๆ proxy_buffering จึงไม่มีผลกับ frame ของ WebSocket ที่ต่อติดแล้ว
ถ้าคุณใช้ Caddy หรือ Traefik ต้องทำอะไร
ไม่ต้องทำอะไรเพิ่ม และนั่นคือความต่างที่ชัดที่สุดข้อหนึ่งระหว่างสามตัวนี้ คำสั่ง reverse_proxy ของ Caddy v2 จัดการ Upgrade ให้เองตั้งแต่บรรทัดแรก ไม่มี map ไม่มี proxy_http_version เพราะ Caddy คุยกับ upstream ด้วย HTTP/1.1 อยู่แล้ว ส่วน Traefik ก็ส่ง Upgrade ต่อให้ router ของมันโดยไม่ต้องตั้ง middleware ใด ๆ
สิ่งที่ยังต้องตั้งเองในทั้งสองตัวคือ timeout สำหรับ connection ที่อยู่ยาว กับการบอก path ที่ถูกต้องให้แอป ตรรกะของหัวข้อ proxy_read_timeout ข้างบนใช้ได้เหมือนกัน เปลี่ยนแค่ชื่อของ directive ถ้ากำลังชั่งใจว่าจะย้ายหรือไม่ การเทียบ nginx, Caddy และ Traefik แบบลงรายละเอียด แจกแจงไว้ว่าคุณกำลังแลกอะไรกับอะไร งานที่หายไปตรงนี้ไม่ได้ฟรี มันแลกมาด้วยการควบคุมที่ละเอียดน้อยลงในที่อื่น
คำถามที่พบบ่อย (FAQ)
ทำไมต้องใช้ map แทนการเขียน Connection เป็น upgrade ไปตรง ๆ
เพราะ location เดียวกันมักรับทั้ง WebSocket และ HTTP ธรรมดา การเขียน proxy_set_header Connection "upgrade"; แบบตายตัวจะติด header นี้ไปกับทุกคำขอ รวมถึงคำขอที่ไม่ได้ขอเปลี่ยนโปรโตคอลเลย และยังทำให้ keepalive ระหว่าง nginx กับ upstream ใช้ไม่ได้ เพราะค่า Connection ถูกเขียนทับ ส่วน map $http_upgrade $connection_upgrade จะให้ค่า upgrade เฉพาะคำขอที่มี header Upgrade เข้ามาจริง และให้ค่า close กับคำขออื่น บล็อก map ต้องอยู่ที่ระดับ http เท่านั้น วางใน server หรือ location แล้ว nginx จะไม่โหลด config
ควรตั้ง proxy_read_timeout เป็นเท่าไร
ค่าเริ่มต้นคือ 60 วินาที ซึ่งสั้นเกินไปสำหรับ WebSocket ที่เงียบอยู่เฉย ๆ ถ้าแอปของคุณส่ง ping frame เองเป็นระยะ ตั้งให้ยาวกว่าช่วงห่างของ ping สักสองถึงสามเท่าก็พอ ถ้าแอปไม่ส่ง ให้ตั้งยาวอย่าง 3600s แล้วยอมรับว่า connection ที่ตายแล้วจะค้างอยู่นานขึ้นก่อนถูกเก็บ ทางที่สมดุลที่สุดคือแยก location ของ path WebSocket ออกมาแล้วตั้งค่ายาวเฉพาะตรงนั้น ส่วน location ที่รับ HTTP ปกติปล่อยไว้ตามค่าเดิม เพื่อให้คำขอที่ upstream ค้างยังล้มเร็วเหมือนเดิม
ต้องแยก location สำหรับ WebSocket ไหม
ไม่จำเป็น การใส่ proxy_http_version 1.1 กับ header สองตัวไว้ใน location / ทำงานได้ เพราะ map ทำให้คำขอที่ไม่ใช่ WebSocket ไม่ได้รับผลกระทบ เหตุผลเดียวที่ควรแยกคือเรื่อง timeout ตามที่อธิบายไว้ในคำถามก่อนหน้า ถ้าแยก ให้ระวังว่า nginx เลือก location ที่ตรงที่สุด และ proxy_set_header ไม่สืบทอดลงมาถ้า location ปลายทางมี proxy_set_header ของตัวเองอยู่แล้ว ต้องเขียน header ให้ครบชุดในทุก location ที่ proxy คำขอออกไป
หน้าเว็บโหลดได้แต่ WebSocket ไม่ติด ปัญหาอยู่ที่ nginx หรือที่แอป
แยกด้วยการทดสอบสองรอบ รอบแรกยิง handshake ผ่าน nginx ที่โดเมนจริง รอบที่สองยิงตรงไปที่พอร์ตของแอปบน localhost ด้วยคำสั่ง curl ชุดเดียวกัน ถ้าสองรอบให้ผลต่างกัน ปัญหาอยู่ที่ nginx และ directive ที่ต้องดูคือ proxy_http_version กับ proxy_set_header สองตัวของ WebSocket ถ้าทั้งสองรอบให้ผลเหมือนกัน nginx ไม่ได้เกี่ยว ให้ไปดู log ของแอปเองและการตั้งค่า public URL ของมัน อีกกรณีที่หลอกตาคือแอปตอบถูกทุกอย่างแต่เบราว์เซอร์บล็อกเอง เพราะหน้าเว็บเป็น https แล้วแอปสั่งให้ต่อไปที่ ws:// ซึ่งเกิดจาก X-Forwarded-Proto ที่ไม่ได้ส่งขึ้นไป