Postel’s Law
ใจกว้างกับสิ่งที่รับเข้า เข้มงวดกับสิ่งที่ส่งออก
ใจความสำคัญ
หัวข้อที่มีชื่อว่า “ใจความสำคัญ”Postel’s Law หรือที่รู้จักกันในชื่อ Robustness Principle ตั้งชื่อตาม Jon Postel วิศวกรผู้บุกเบิกอินเทอร์เน็ต ซึ่งเขียนหลักการนี้ไว้ครั้งแรกใน spec IPv4 ปี 1979 และปรากฏอีกครั้งใน RFC 761 (มกราคม 1980) ซึ่งเป็น spec ยุคแรกของ TCP
“Be conservative in what you do, be liberal in what you accept from others.” (จงเข้มงวดในสิ่งที่คุณทำ และใจกว้างในสิ่งที่คุณรับเข้ามาจากผู้อื่น)
— Jon Postel, RFC 761
มักถูกพูดถึงใน version สั้นกว่าว่า “จงเข้มงวดในสิ่งที่คุณส่งออก และใจกว้างในสิ่งที่คุณรับเข้ามา” (be conservative in what you send, be liberal in what you accept) หลักการนี้ถูกเขียนขึ้นเพื่อให้ implementation ของ TCP ต่างค่ายที่สื่อสารกันผ่าน spec ที่ยังไม่สมบูรณ์และคลุมเครือ สามารถทำงานร่วมกันได้จริง แทนที่จะพังเพราะตีความรายละเอียดปลีกย่อยต่างกัน
เป้าหมายคือลดความล้มเหลวของระบบที่เชื่อมต่อกัน การยอมรับ input หลายรูปแบบ (เช่น endpoint ที่รับทั้ง XML และ JSON หรือ parser ที่ยอมรับ header ที่เขียนผิดเล็กน้อย) ทำให้ระบบทำงานร่วมกับ client ที่หลากหลายได้ ขณะที่การส่ง output ที่สม่ำเสมอและตรง spec ทำให้ระบบอื่นที่เรียกใช้ไม่ต้องมาคอยเดาหรือเขียน code พิเศษรับมือความไม่แน่นอนจากฝั่งเรา หลักการนี้จึงเป็นเสาหลักต้นๆ ที่ทำให้อินเทอร์เน็ตยุคแรกขยายตัวและอยู่รอดได้ ทั้งที่ spec จำนวนมากยังไม่รัดกุมและ implementation แต่ละเจ้าก็มี bug ของตัวเอง
ความหมายและนัยยะ
หัวข้อที่มีชื่อว่า “ความหมายและนัยยะ”ฝั่งรับ (accept): โปรแกรมที่รับข้อความควรยอมรับ input ที่ไม่ตรง spec ทุกกระเบียดนิ้ว ตราบใดที่ความหมาย (semantics) ยังชัดเจน เช่น ยอมรับ field ที่เรียงลำดับผิด ยอมรับค่าที่ขาดหายไปโดยใช้ default ที่สมเหตุสมผล หรือเพิกเฉยต่อ field แปลกปลอมที่ตนเองไม่รู้จักแทนที่จะโยน exception
ฝั่งส่ง (send): โปรแกรมที่ส่งข้อความควรเคารพ spec อย่างเคร่งครัดที่สุด ไม่ใช้ shortcut หรือ feature นอก spec แม้ระบบปลายทางจะรองรับได้ในทางปฏิบัติ เพราะ output ที่ “เดาไม่ยาก” และไม่สร้างเซอร์ไพรส์ให้ผู้เรียกคือสิ่งที่ทำให้ระบบอื่นวางใจเชื่อมต่อกับเราได้ (แนวคิดนี้เกี่ยวโยงโดยตรงกับ Principle of Least Astonishment)
ในเชิงทฤษฎีชนิดข้อมูล (type theory) หลักการนี้สะท้อนแนวคิด covariance/contravariance ของ function: function ควรจะ contravariant ต่อชนิดของ input (ยอมรับ supertype ที่กว้างกว่าที่คาดไว้ได้) แต่ covariant ต่อชนิดของ output (คืนค่าที่จำเพาะเจาะจงกว่าหรือเท่ากับที่สัญญาไว้เท่านั้น) — เป็นภาพสะท้อนของ “ใจกว้างตอนรับ เข้มงวดตอนส่ง” ในระดับ signature ของ code
Martin Fowler นำหลักการนี้มาเป็นฐานของ pattern ที่เรียกว่า Tolerant Reader: เวลาอ่านข้อมูลจากระบบภายนอก (เช่น XML/JSON response) ให้ดึงเฉพาะ field ที่ต้องใช้จริง ใช้ query แบบยืดหยุ่น (เช่น //order แทน path ตายตัวอย่าง /order-history/order-list/order) และเพิกเฉยต่อโครงสร้างส่วนที่ไม่รู้จัก วิธีนี้ทำให้ผู้ให้บริการ (provider) ปรับโครงสร้างข้อมูลได้โดยไม่ทำให้ผู้บริโภค (consumer) พังไปด้วย
อย่างไรก็ตาม หลักการนี้ไม่ได้ถูกยอมรับแบบไร้ข้อกังขาตลอดมา:
- RFC 1122 (1989) โดย Bob Braden ขยายความคิดของ Postel โดยแนะนำให้นักพัฒนา “สมมติว่าเครือข่ายเต็มไปด้วยฝ่ายร้ายที่จะส่ง packet ที่ออกแบบมาให้สร้างผลเสียที่สุดเท่าที่จะทำได้” — ระบบไม่ควรแค่ “รอด” จาก host ที่พฤติกรรมผิดปกติ แต่ต้องจำกัดความเสียหายที่จะลามไปยังส่วนอื่นของเครือข่ายที่ใช้ร่วมกันด้วย
- Eric Allman, ผู้เขียน sendmail, เขียนบทความ “The Robustness Principle Reconsidered” ใน ACM Queue (2011) ชี้ว่าการ “ใจกว้างเกินไป” กลายเป็นช่องโหว่ด้านความปลอดภัยในโลกที่อินเทอร์เน็ตเต็มไปด้วยผู้ไม่หวังดี — การยอมรับ input ที่ไม่ตรง spec อย่างไม่ระวังเปิดช่องให้เกิดปัญหาอย่าง DNS poisoning, connection hijacking หรือ input ที่ถูกออกแบบมาโจมตีโดยเฉพาะ เขายกตัวอย่างเบราว์เซอร์ที่ยอมรับ HTML ผิด spec จนกลายเป็น “มาตรฐานโดยพฤตินัย” ที่ทุกเบราว์เซอร์ต้องเลียนแบบ bug ตามกันไปเรื่อยๆ ข้อเสนอของเขาคือใช้หลักการนี้อย่างมีบริบท ไม่ใช่ยึดเป็นความจริงตายตัว
- ในปี 2023 เอกสาร RFC 9413 (Thomson และ Schinazi) ถกว่าหลักการนี้ในทางปฏิบัติกลับบั่นทอนความทนทานของระบบ เพราะข้อบกพร่องของ implementation หนึ่งที่ถูกฝั่งรับ “ใจกว้าง” ยอมรับไปเรื่อยๆ จะกลายเป็นพฤติกรรมที่ implementation อื่นทุกตัวต้องเลียนแบบตามเพื่อ interoperability (bug-for-bug compatibility) ทำให้แก้ไขปัญหาที่ต้นตอได้ยากขึ้นเรื่อยๆ ตามเวลา
สรุปคือ Postel’s Law ยังคงเป็นแนวทางที่มีประโยชน์มากในการออกแบบ integration point แต่ต้องใช้คู่กับสามัญสำนึกด้านความปลอดภัย: “ใจกว้าง” ไม่ได้แปลว่ายอมรับทุกอย่างโดยไม่ตรวจสอบ
ตัวอย่างในโลกจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างในโลกจริง”เว็บเบราว์เซอร์กับ HTML: เบราว์เซอร์แทบทุกตัวยอมพาร์ส HTML ที่ tag ไม่ปิด, attribute ไม่มี quote หรือโครงสร้างผิดเพี้ยน แล้วพยายามเดาเจตนาของผู้เขียนหน้าเว็บ นี่คือเหตุผลที่เว็บยุคแรกๆ ที่เขียนสไตล์หลวมๆ ยังเปิดดูได้ทุกวันนี้ แต่ผลข้างเคียงคือพฤติกรรม “เดาใจ” ของแต่ละเบราว์เซอร์กลายเป็นมาตรฐานที่ implicit ต้องตามกันไป
Sendmail และอีเมล: Eric Allman เล่าประสบการณ์ตรงว่า code “แก้ไข address” ของ sendmail ที่ตั้งใจใจกว้างกับ input ทำงานได้ดีในสภาพแวดล้อมหนึ่ง แต่กลับสร้างปัญหาเมื่อย้ายไปรันบน hosting ที่ต่างบริบท เพราะมันเดาผิดในสถานการณ์ที่ไม่เคยเจอ
HTTP API ที่รับได้หลาย format: endpoint ที่รับได้ทั้ง Content-Type: application/json และ application/xml, หรือ deserializer ที่ไม่ล้มเมื่อเจอ field แปลกปลอมในการตอบกลับของ 3rd-party API เป็นการนำ Postel’s Law มาปรับใช้ในระดับสถาปัตยกรรมสมัยใหม่ ตัวอย่าง Tolerant Reader ใน code:
// ฝั่งรับ (deserialize): ใจกว้าง — เพิกเฉย field ที่ไม่รู้จัก, ใช้ default เมื่อขาดหายpublic class OrderReader{ public OrderSummary Parse(JsonElement payload) { // ดึงเฉพาะ field ที่ต้องใช้จริง ไม่ผูกกับ schema ทั้งหมดของผู้ให้บริการ var id = payload.GetProperty("orderId").GetString() ?? string.Empty; var total = payload.TryGetProperty("total", out var t) ? t.GetDecimal() : 0m; // default ที่สมเหตุสมผลเมื่อ field ขาดหาย // field อื่น ๆ ที่ผู้ให้บริการอาจเพิ่มเข้ามาในอนาคตจะถูกละเว้นโดยอัตโนมัติ return new OrderSummary(id, total); }}
// ฝั่งส่ง (serialize): เข้มงวด — ส่งออกตาม schema ที่ตกลงกันเท่านั้น พร้อม validate ก่อนส่งเสมอpublic class OrderWriter{ public string Serialize(OrderSummary order) { if (string.IsNullOrEmpty(order.Id)) throw new InvalidOperationException("OrderId ต้องไม่ว่างก่อนส่งออก");
return JsonSerializer.Serialize(new { orderId = order.Id, total = order.Total }); }}โครงสร้างนี้แสดง asymmetry ที่เป็นหัวใจของกฎ: ฝั่งอ่าน (OrderReader) ยืดหยุ่นและทนทานต่อการเปลี่ยนแปลงของผู้ให้บริการ ส่วนฝั่งเขียน (OrderWriter) เข้มงวดและ validate ก่อนส่งออกเสมอ เพื่อไม่ให้ผู้บริโภคของเราต้องมาปวดหัวกับ output ที่ไม่แน่นอน
ภาพรวมของ flow นี้สรุปได้ดังนี้:
flowchart LR
A[Input หลายรูปแบบจากภายนอก] --> B[Parse อย่างยืดหยุ่น]
B --> C[Validate และ Normalize]
C --> D[Domain Model ภายใน]
D --> E[Serialize อย่างเข้มงวด]
E --> F[Output ตรง spec เดียวเสมอ]
บทเรียนที่นำไปใช้
หัวข้อที่มีชื่อว่า “บทเรียนที่นำไปใช้”- แยกจุดที่รับข้อมูลกับจุดที่ส่งข้อมูลให้ชัด แล้วใช้กติกาต่างกัน: ตัว deserializer/parser ควรทนทานและให้ default ที่ปลอดภัย ส่วน serializer/validator ก่อนส่งออกควรเข้มงวดและปฏิเสธข้อมูลที่ไม่สมบูรณ์
- อย่าปล่อยให้ “ใจกว้าง” กลายเป็น “ไม่ตรวจสอบเลย” โดยเฉพาะกับข้อมูลที่กระทบความปลอดภัย (เช่น ค่าที่จะถูกนำไป query, exec, หรือ deserialize เป็น object) ตามคำเตือนของ Eric Allman ควรตรวจสอบ input ที่มาจากภายนอกอย่างเข้มงวดในจุดที่มีความเสี่ยง แม้จะยอมรับรูปแบบหลากหลายในจุดที่ไม่เสี่ยง
- ใช้ Tolerant Reader ร่วมกับ Anti-Corruption Layer เมื่อ integrate กับระบบภายนอกหรือ legacy system ให้มีจุดแปลข้อมูลจุดเดียวที่รับผิดชอบความยืดหยุ่นทั้งหมด แทนที่จะกระจายตรรกะ “เผื่อกรณีแปลกๆ” ไปทั่วทั้ง codebase
- share contract test กับผู้ให้บริการ แนวทางที่ Fowler แนะนำคือให้ผู้บริโภคเขียน test ที่จำลอง response ที่คาดหวัง แล้ว share กับทีมผู้ให้บริการ เพื่อให้รู้ทันทีเมื่อการเปลี่ยนแปลงฝั่งใดฝั่งหนึ่งจะทำให้อีกฝั่งพัง — เป็นต้นกำเนิดของแนวคิด Consumer-Driven Contracts
- อย่าปล่อยให้หนี้ความเข้ากันได้สะสมนานเกินไป implementation ที่ผิด spec แต่ถูกฝั่งรับใจกว้างยอมรับไปเรื่อยๆ จะกลายเป็นพฤติกรรมที่ต้องรองรับตลอดไป ยิ่งปล่อยนานยิ่งแก้ยาก ดังนั้นควร log/แจ้งเตือนเมื่อรับ input ที่ผิด spec แม้จะยอมให้ผ่านไปก็ตาม เพื่อให้เห็นภาพหนี้เทคนิคที่สะสมอยู่