ข้ามไปยังเนื้อหา

Production Server

ทุกบทก่อนหน้าในโมดูลนี้ครอบคลุมทีละ guard: error listener ที่นี่, body limit ที่นั่น, graceful shutdown handler ที่อื่น server ใน production จริงต้องการ ทุกอย่างพร้อมกัน ในไฟล์เดียวกัน ทำงานร่วมกันแทนที่จะขวางกันเอง บทนี้คือ server หนึ่งตัวที่ทำแบบนั้นเป๊ะ — ทุก guard จากโมดูลนี้ ต่อเข้าด้วยกันใน request handler เดียว พร้อม request ที่ทดสอบแต่ละ guard

Node.js
Output (verified on Node 24)
{"method":"GET","path":"/health","status":200,"clientIp":"::1"}
{"method":"POST","path":"/items","status":413}
{"method":"POST","path":"/items","status":400,"clientIp":"::1"}
{"method":"POST","path":"/items","status":201,"clientIp":"::1"}
{"level":"info","msg":"SIGTERM received, closing server"}
{"level":"info","msg":"server closed cleanly"}

request สี่ตัว guard สี่แบบต่างกัน server เดียว: health check ธรรมดา, body ที่เกิน MAX_BODY (413 ผ่าน pattern req.destroy() จากบท body limit), JSON body ที่ผิดรูปแบบ (400 ผ่าน try/catch รอบ JSON.parse) และ request ที่ถูกต้องที่สร้าง item จริง (201) จากนั้น SIGTERM จะ trigger server.close() ซึ่ง log เมื่อเสร็จแล้ว — เพราะไม่มีอะไรค้างอยู่ระหว่างทำงาน มันจึงปิดทันที แต่ handler เดียวกันนี้จะรอ response ที่กำลังทำอยู่ให้เสร็จก่อนถ้ามีจริง เหมือนกับบท graceful-shutdown เป๊ะ

Guardมาจากบทไหนในโมดูลนี้หยุดอะไร
MAX_BODY + req.destroy()บท JSON APIการ upload ที่ไม่จำกัดขนาดที่ทำให้ memory โตไม่หยุด
try/catch รอบ JSON.parseบท JSON APISyntaxError ที่กลายเป็น unhandled rejection
res.on('error', ...)บท HTTP Serverstream error (เช่น double res.end()) ที่ทำให้ทั้ง process crash
if (res.writableEnded) return; ใน finish()บท HTTP Serverการเรียก res.end() ครั้งที่สองบน response เดียวกัน
res.setHeader('X-Content-Type-Options', 'nosniff')บทนี้browser MIME-sniff response ให้กลายเป็น content type อื่นที่อาจอันตราย
อ่าน req.socket.remoteAddress ไม่เชื่อ X-Forwarded-For แบบไม่คิดบทนี้header client-IP ปลอมเมื่อไม่มี reverse proxy ที่ตั้งค่ามันจริง ๆ
process.once('SIGTERM', shutdown) + server.close()บท HTTP Serverการทิ้ง request ที่กำลังทำอยู่เมื่อ process manager ขอให้ server หยุด
เรียก log(...) หนึ่งครั้งต่อ branch เป็น JSON ที่มีโครงสร้างบทนี้กำแพง console.log ที่ไม่มีโครงสร้างซึ่งไม่มีอะไร parse ต่อได้เลย

X-Content-Type-Options: nosniff บอก browser ให้เชื่อ Content-Type ที่คุณประกาศไว้แทนการเดาจาก body — ถ้าไม่มีมัน browser ที่ระบุ response JSON หรือ text ผิดว่าเป็น HTML อาจเปิดช่องโหว่ XSS ที่ Content-Type ที่ถูกต้องอย่างเดียวจะป้องกันได้ สำหรับ server ที่รันบน TLS เท่านั้น Strict-Transport-Security (HSTS) ก็คุ้มค่าที่จะเพิ่มด้วย เพื่อบอก browser ว่าอย่า downgrade กลับไปใช้ HTTP ธรรมดาสำหรับ domain นี้อีก — แต่ตั้งค่ามันก็ต่อเมื่อคุณมั่นใจว่าทุก path จริง ๆ ถูก serve ผ่าน HTTPS เพราะมันเป็นสัญญาที่ browser จะยึดคุณไว้

X-Forwarded-For (และ X-Forwarded-Proto, X-Forwarded-Host) เป็น header ที่ reverse proxy ตั้งเพื่อบอก app ของคุณว่า address และ protocol ของ client ต้นทางจริง ๆ คืออะไร เพราะ IP ของ proxy เองคือสิ่งที่ server ของคุณเห็นบน socket จริง ๆ จุดที่ต้องระวัง: client ใดก็ตามสามารถตั้ง header พวกนี้เองใน request ตรง ๆ ได้ การเชื่อมันแบบไม่คิด (สำหรับ rate limiting, access control หรือ logging) หมายความว่า client อ้างเป็น IP อะไรก็ได้ตามใจ ควรเชื่อมันก็ต่อเมื่อคุณรู้ว่า proxy ที่คุณควบคุมเป็นสิ่งเดียวที่เข้าถึง process ของคุณตรง ๆ ได้ และแม้ตอนนั้นก็ควรเชื่อแค่ hop ล่าสุด ไม่ใช่ค่าเต็มของ header ที่ client อาจ prepend มาก่อนหน้าได้

server ใน production ไม่ใช่ feature ใหญ่หนึ่งตัว — มันคือสัญญา request/response แบบเดียวกับบท HTTP Server ที่ห่อด้วยชุด guard อิสระที่แต่ละตัวตอบคำถามข้อเดียว: input ใหญ่เกินไปไหม? มันเป็น JSON ที่ valid ไหม? มีอะไร throw ไหม? กำลังจะ shutdown อยู่หรือเปล่า? ไม่มี guard ไหนขึ้นกับตัวอื่นเลย ซึ่งนั่นแหละคือสิ่งที่ทำให้มันประกอบกันเป็น handler เดียวได้ โดยไม่มีตัวไหนซับซ้อนกว่าตัวอื่น

ทุก guard ในบทนี้คือโค้ดจำนวนน้อยที่ไม่ทำอะไรเลยในกรณีปกติ และสำคัญเฉพาะวันที่มีอะไรผิดพลาด — ซึ่งทำให้มันข้ามไปได้ง่ายเมื่อมีเวลาจำกัด และลืมไปว่ามันขาดหายจนกว่าจะมี incident มาทำให้เห็นชัด helper finish() และการเรียก log() แบบมีโครงสร้างในบทนี้ก็เข้ารหัสชุดการตัดสินใจเฉพาะ (JSON log, response helper ตัวเดียว) ที่ codebase จริงอาจเลือกต่างออกไป — ประเด็นไม่ใช่รูปแบบนี้เป๊ะ ๆ แต่ server ใน production ต้องมีคำตอบที่สม่ำเสมอและรวมศูนย์บางแบบสำหรับ “จะจบ response ยังไง” และ “จะ log request ยังไง” แทนที่แต่ละ route จะคิดค้นทั้งสองอย่างใหม่เอง

  • “Production-readiness คือ phase แยกที่เพิ่มตอนท้าย” ทุก guard ในบทนี้เล็กและอิสระ — เวลาที่ถูกที่สุดในการเพิ่ม body limit หรือ error listener คือตอนเขียน route ไม่ใช่ตอน retrofit หลังจากมีอะไรพังใน production ไปแล้ว
  • “Structured logging แปลว่าต้องใช้ library อย่าง pino” โครงสร้าง (JSON object หนึ่งตัวต่อ event, field ที่สม่ำเสมอ) สำคัญกว่า library ที่ใช้ — demo ด้านบนได้ความสามารถ query แบบเดียวกับที่ logging library จะให้ จาก console.log(JSON.stringify(...)) เพียงแต่ไม่มี feature เสริม (level, transport, sampling) ที่ library จริงเพิ่มให้
  • “X-Forwarded-For บอก IP จริงของ client เสมอ” จริงก็ต่อเมื่อคุณควบคุม proxy ที่ตั้งมันและไม่มีอะไรอื่นเข้าถึง server ได้ตรง ๆ ไม่งั้นมันก็น่าเชื่อถือพอ ๆ กับ header อื่นที่ client ตั้งเองได้

server “production-ready” ที่ AI เขียนมักเพิ่มสิ่งดี ๆ จริง — body limit ตรงนี้, try/catch ตรงนั้น — โดยไม่เชื่อมมันเข้าด้วยกัน: branch body-too-large ที่เขียน response แล้วลืม guard ไม่ให้ finish() รันซ้ำอีกทีหลังจากนั้น เป็นวิธีที่แนบเนียนที่จะทำให้ crash แบบ double-res.end() ที่โมดูลนี้พยายามป้องกันมาตลอดกลับมาอีกครั้ง

🐛 หา bug ในโค้ดนี้

function finish(req, res, status, body) {
const json = JSON.stringify(body);
res.writeHead(status, {
'Content-Type': 'application/json',
'Content-Length': json.length, // BUG: character length, not byte length
});
res.end(json);
}
// Called with a name containing multi-byte characters
finish(req, res, 201, { id: 42, name: 'José 🎉' });
เฉลย

json.length นับ UTF-16 code unit ไม่ใช่ byte — สำหรับ body ที่มีตัวอักษรมี accent หรือ emoji (หรือข้อความไทยแบบตัวอย่าง byte-length ก่อนหน้านี้ในโมดูล) จำนวน byte จริงของ UTF-8 (สิ่งที่ Content-Length ต้องประกาศ) มากกว่าที่ .length รายงาน client จะอ่านตาม Content-Length เป๊ะแล้วถือว่า response เสร็จตรงนั้น ทำให้มันตัด body ทิ้งกลางตัวอักษร multi-byte แบบเงียบ ๆ และขึ้นอยู่กับ HTTP client ขอบเขต byte ที่พังนี้ยังอาจทำให้ response ถัดไปบน keep-alive connection ที่ใช้ซ้ำ desync ได้ด้วย ทางแก้จากบท HTTP Server: ใช้ Buffer.byteLength(json) แทน json.length — หรือดีกว่านั้นคือปล่อยให้ res.end(json) คำนวณ Content-Length เองโดยไม่ตั้งมันด้วยมือเลย

ใน server ที่รวมกันแล้ว ทำไม handler ของ POST /items ถึงเช็ค err.handled ก่อนเรียก finish(500, ...)?
if (res.writableEnded) return; ข้างใน finish() ป้องกันอะไร?
จาก 4 request ที่ demo ยิงออกไป (GET /health, POST เกินขนาด, JSON ผิดรูปแบบ และ POST ที่ถูกต้อง) status code ที่ log ตัวที่ 2 คืออะไร?
ทำไม server.close() ถึงเสร็จทันทีใน demo นี้แทนที่จะรอ?
process.emit('SIGTERM');
server.closeAllConnections();
ทำไม X-Forwarded-For ถึงไม่ปลอดภัยที่จะเชื่อสำหรับ access control เว้นแต่คุณควบคุม proxy ที่ตั้งมัน?
handler ตั้ง Content-Length: json.length เองสำหรับ response body ที่มีข้อความไทย จะเกิดอะไรผิดพลาด?
const json = JSON.stringify({ name: 'สมชาย ใจดี' });
res.writeHead(200, { 'Content-Length': json.length });