รูปแบบสัญญาข้อผิดพลาดของ API เพื่อข้อความที่ชัดเจนและเป็นมิตร
ออกแบบสัญญาข้อผิดพลาดของ API โดยใช้รหัสที่เสถียร ข้อความแปลตามภาษา และคำแนะนำที่เหมาะกับ UI เพื่อลดภาระฝ่ายสนับสนุนและช่วยให้ผู้ใช้กู้สถานะได้อย่างรวดเร็ว

ทำไมข้อผิดพลาด API ที่ไม่ชัดเจนถึงสร้างปัญหาให้ผู้ใช้จริงๆ
ข้อผิดพลาด API ที่กำกวมไม่ใช่แค่ความรำคาญเชิงเทคนิค แต่มันคือช่วงเวลาที่ประสบการณ์ผู้ใช้ขาดตอน ผู้ใช้ติดขัด คาดเดาวิธีแก้ แล้วมักจะยอมแพ้ ข้อความเดียวอย่าง "Something went wrong" กลายเป็นตั๋วซัพพอร์ตเพิ่ม อัตราการยกเลิกบริการสูงขึ้น และบั๊กที่ดูเหมือนไม่เคยแก้ได้จริง
รูปแบบที่พบบ่อยคือ: ผู้ใช้พยายามบันทึกฟอร์ม UI แสดง toast แบบทั่วไป แต่บันทึกแบ็กเอนด์บอกสาเหตุจริง (เช่น "unique constraint violation on email") ผู้ใช้ไม่รู้ว่าควรเปลี่ยนอะไร ฝ่ายซัพพอร์ตช่วยไม่ได้เพราะไม่มีรหัสที่ค้นหาได้ในล็อก ปัญหาเดิมถูกรายงานซ้ำในภาพหน้าจอและข้อความต่างๆ และไม่มีวิธีจัดกลุ่มที่ชัดเจน
รายละเอียดเชิงวิศวกรกับความต้องการของผู้ใช้ไม่เหมือนกัน วิศวกรต้องการบริบทล้มเหลวที่แม่นยำ (ฟิลด์ไหน บริการไหน เวลาออก) ส่วนผู้ใช้ต้องการขั้นตอนถัดไปที่ชัดเจน: "อีเมลนี้ถูกใช้งานแล้ว ลองเข้าสู่ระบบ หรือลองใช้อีเมลอื่น" ผสมสองสิ่งนี้มักจบลงด้วยการเปิดเผยข้อมูลภายในหรือข้อความที่ไร้ประโยชน์
นั่นคือเหตุผลที่ต้องมีสัญญาข้อผิดพลาดของ API เป้าหมายไม่ใช่ "ให้มีข้อผิดพลาดมากขึ้น" แต่เป็นโครงสร้างที่สอดคล้องเพื่อให้:
- ไคลเอ็นต์ตีความความล้มเหลวได้อย่างน่าเชื่อถือในทุก endpoint
- ผู้ใช้เห็นข้อความที่ปลอดภัย เป็นภาษาง่าย และช่วยให้กู้สถานะได้
- ฝ่ายซัพพอร์ตและ QA ระบุปัญหาได้ชัดเจนด้วยรหัสที่เสถียร
- วิศวกรได้รับข้อมูลวินิจฉัยโดยไม่เปิดเผยข้อมูลสำคัญ
ความสม่ำเสมอคือทั้งหมด หาก endpoint หนึ่งคืนค่า error: "Invalid" และอีก endpoint คืน message: "Bad request" UI จะนำทางผู้ใช้ไม่ได้ และทีมของคุณวัดอะไรไม่ได้ สัญญาที่ชัดเจนทำให้ข้อผิดพลาดคาดเดาได้ ค้นหาได้ และแก้ไขได้ง่ายขึ้น แม้สาเหตุเบื้องหลังจะเปลี่ยน
สัญญาข้อผิดพลาดที่สอดคล้องในทางปฏิบัติหมายถึงอะไร
สัญญาข้อผิดพลาดของ API คือคำสัญญา: เมื่อเกิดความล้มเหลว API ของคุณตอบกลับในรูปแบบที่คุ้นเคยด้วยฟิลด์และรหัสที่คาดเดาได้ ไม่ว่าจะเป็น endpoint ใดก็ตาม
มันไม่ใช่การปล่อยข้อมูลสำหรับดีบัก และไม่ใช่แทนที่ล็อก สัญญาคือสิ่งที่แอพไคลเอ็นต์พึ่งพาได้อย่างปลอดภัย ล็อกเก็บสแตกเทรซ รายละเอียด SQL และข้อมูลสำคัญอื่นๆ
ในทางปฏิบัติ สัญญาที่ดีคงสิ่งต่อไปนี้ไว้ให้เสถียร: รูปร่างการตอบกลับข้าม endpoint (สำหรับ 4xx และ 5xx), รหัสเครื่องอ่านที่ไม่เปลี่ยนความหมาย, และข้อความสำหรับผู้ใช้ที่ปลอดภัย มันยังช่วยฝ่ายซัพพอร์ตด้วยการรวม request/trace identifier และอาจมีคำแนะนำ UI ง่ายๆ เช่น ควรลองใหม่หรือแก้ฟิลด์ใด
ความสม่ำเสมอเกิดขึ้นได้เมื่อคุณตัดสินใจว่าจะบังคับใช้ที่ไหน ทีมมักเริ่มจากจุดบังคับใช้หนึ่งจุดแล้วขยาย: API gateway ที่ทำให้ข้อผิดพลาดเป็นมาตรฐาน, middleware ที่ห่อความล้มเหลวที่ไม่ได้จับ, ไลบรารีแชร์ที่สร้างออบเจ็กต์ข้อผิดพลาดเดียวกัน, หรือ exception handler ระดับเฟรมเวิร์กในแต่ละบริการ
ความคาดหวังหลักง่ายๆ: ทุก endpoint คืนหรือรูปแบบสำเร็จหรือสัญญาข้อผิดพลาดสำหรับทุกโหมดความล้มเหลว นั่นรวมการตรวจสอบความถูกต้อง การพิสูจน์ตัวตน ขีดจำกัดอัตรา เวลาออก และการล้มเหลวจาก upstream
รูปร่างการตอบข้อผิดพลาดที่เรียบง่ายแต่ขยายได้
สัญญาข้อผิดพลาดที่ดีต้องกระชับ คาดเดาได้ และเป็นประโยชน์ทั้งกับคนและซอฟต์แวร์ เมื่อไคลเอ็นต์หาฟิลด์เดิมได้เสมอ ฝ่ายซัพพอร์ตจะหยุดเดา และ UI จะให้คำแนะนำที่ชัดเจน
นี่คือรูปแบบ JSON ขั้นต้นที่ใช้ได้กับผลิตภัณฑ์ส่วนใหญ่ (และขยายได้เมื่อเพิ่ม endpoint):
{
"status": 400,
"code": "AUTH.INVALID_EMAIL",
"message": "Enter a valid email address.",
"details": {
"fields": {
"email": "invalid_email"
},
"action": "fix_input",
"retryable": false
},
"trace_id": "01HZYX8K9Q2..."
}
เพื่อให้สัญญาเสถียร จัดการแต่ละส่วนเป็นคำสัญญาแยกกัน:
statusสำหรับพฤติกรรม HTTP และหมวดกว้างๆcodeเป็นตัวระบุเครื่องอ่านที่เสถียร (แกนกลางของสัญญาข้อผิดพลาดของคุณ)messageคือข้อความปลอดภัยสำหรับ UI (และแปลได้ภายหลัง)detailsเก็บคำแนะนำเชิงโครงสร้าง: ปัญหาระดับฟิลด์ สิ่งที่ควรทำต่อไป และว่าควรลองใหม่หรือไม่trace_idให้ฝ่ายซัพพอร์ตค้นหาความล้มเหลวฝั่งเซิร์ฟเวอร์โดยไม่เปิดเผยข้อมูลภายใน
แยกเนื้อหาที่มองเห็นได้สำหรับผู้ใช้จากข้อมูลดีบักภายใน หากต้องการวินิจฉัยเพิ่มเติม ให้บันทึกลงล็อกเชื่อมกับ trace_id (ไม่ใส่ในการตอบกลับ) จะหลีกเลี่ยงการรั่วไหลของข้อมูลที่ละเอียดอ่อน แต่ยังช่วยให้สืบหาปัญหาได้ง่าย
สำหรับข้อผิดพลาดของฟิลด์ details.fields เป็นรูปแบบง่ายๆ: คีย์ตรงกับชื่ออินพุต ค่าเก็บเหตุผลสั้นๆ เช่น invalid_email หรือ too_short เพิ่มคำแนะนำเมื่อตรงส่วนที่จะช่วยได้ สำหรับ timeout ใช้ action: "retry_later" ก็เพียงพอ สำหรับการขัดข้องชั่วคราว retryable: true ช่วยให้ไคลเอ็นต์ตัดสินใจว่าจะโชว์ปุ่มลองใหม่หรือไม่
หมายเหตุก่อนเริ่ม: บางทีมห่อข้อผิดพลาดในวัตถุ error (เช่น { "error": { ... } }) ขณะที่บางทีมเก็บฟิลด์ไว้ระดับบนสุด วิธีใดก็ได้ทำงานได้ สิ่งที่สำคัญคือเลือกซองข้อมูลหนึ่งแบบแล้วใช้ให้สม่ำเสมอทุกที่
รหัสข้อผิดพลาดที่เสถียร: รูปแบบที่ไม่ทำให้ไคลเอ็นต์พัง
รหัสข้อผิดพลาดที่เสถียรคือกระดูกสันหลังของสัญญาข้อผิดพลาด API มันทำให้แอปแดชบอร์ดและทีมซัพพอร์ตรู้จักปัญหาได้ แม้คุณจะปรับข้อความ เพิ่มฟิลด์ หรือปรับ UI
นิยามการตั้งชื่อที่ใช้งานได้จริงคือ:
DOMAIN.ACTION.REASON
ตัวอย่าง: AUTH.LOGIN.INVALID_PASSWORD, BILLING.PAYMENT.CARD_DECLINED, PROFILE.UPDATE.EMAIL_TAKEN รักษาโดเมนให้สั้นและคุ้นเคย (AUTH, BILLING, FILES) ใช้คำกริยาที่อ่านชัด (CREATE, UPDATE, PAY)
ปฏิบัติต่อรหัสเหมือน endpoint: เมื่อเผยแพร่แล้วอย่าเปลี่ยนความหมาย ข้อความที่แสดงแก่ผู้ใช้สามารถปรับให้ดีขึ้นเมื่อเวลาผ่านไป แต่รหัสควรคงที่เพื่อไม่ให้ไคลเอ็นต์พังและให้การวิเคราะห์ยังคงสะอาด
ควรกำหนดด้วยว่ารหัสใดเป็นสาธารณะและรหัสใดเป็นภายใน กฎง่ายๆ: รหัสสาธารณะต้องปลอดภัยสำหรับการแสดง ผลิตภัณฑ์และเอกสาร และใช้โดย UI รหัสภายในเก็บไว้ในล็อกเพื่อดีบัก รหัสสาธารณะหนึ่งรายการอาจแม็พกับสาเหตุภายในหลายแบบ โดยเฉพาะเมื่อผู้ให้บริการภายนอกล้มเหลวด้วยหลายวิธี
การยกเลิกใช้ทำได้ดีที่สุดเมื่อเรียบง่าย หากต้องแทนที่รหัส อย่าใช้ซ้ำแบบเงียบๆ ให้แนะนำรหัสใหม่และประกาศว่ารหัสเก่าถูกเลิกใช้ ให้มีช่วงเวลาที่ทั้งสองอาจปรากฏ หากรวมฟิลด์ deprecated_by ให้ชี้ไปยังรหัสใหม่ (ไม่ใช่ URL)
ตัวอย่าง: รักษา BILLING.PAYMENT.CARD_DECLINED ไว้ แม้ว่าคุณจะปรับข้อความ UI และแยกเป็น "ลองบัตรอื่น" กับ "ติดต่อธนาคาร" ความหมายของรหัสยังคงเดิม ขณะที่คำแนะนำพัฒนาได้
ข้อความแปลโดยไม่เสียความสอดคล้อง
การแปลจะยุ่งยากเมื่อ API คืนประโยคเต็มและไคลเอ็นต์ใช้ข้อความนั้นเป็นตรรกะ วิธีที่ดีกว่าคือรักษาสัญญาให้เสถียรแล้วแปลข้อความปลายทาง ไฟล์แปลช่วยให้ความหมายเดิมข้ามภาษา อุปกรณ์ และเวอร์ชันแอปยังคงเหมือนกัน
ก่อนอื่น ตัดสินใจว่าการแปลเก็บที่ไหน หากต้องการแหล่งเดียวสำหรับเว็บ มือถือ และเครื่องมือซัพพอร์ต ข้อความจากเซิร์ฟเวอร์ช่วยได้ หาก UI ต้องการควบคุมโทนและเลย์เอาต์ ไคลเอ็นต์เก็บการแปลมักง่ายกว่า หลายทีมใช้วิธีผสม: API คืนรหัสคงที่บวก message key และพารามิเตอร์ ไคลเอ็นต์เลือกข้อความแสดงผลที่เหมาะสม
สำหรับสัญญาข้อผิดพลาด คีย์ข้อความมักปลอดภัยกว่าประโยคฝัง API API อาจคืน message_key: "auth.too_many_attempts" พร้อม params: {"retry_after_seconds": 300} UI แปลและฟอร์แมตรายละเอียดโดยไม่เปลี่ยนความหมาย
การจัดรูปพจน์และ fallback สำคัญกว่าที่คิด ใช้ระบบ i18n ที่รองรับกฎพจน์ตามโลคัล ไม่ใช่แค่แบบอังกฤษ "1 vs many" กำหนดลำดับ fallback (เช่น: fr-CA -> fr -> en) เพื่อให้สตริงที่ขาดไม่แสดงหน้าจอว่าง
ข้อควรระวังคือให้ถือว่าข้อความที่แปลเป็นเนื้อหาสำหรับผู้ใช้โดยเคร่งครัด อย่าใส่สแตกเทรซ ไอดีภายใน หรือรายละเอียด "ทำไมถึงล้มเหลว" ลงในสตริงแปล เก็บรายละเอียดสำคัญไว้ในฟิลด์ที่ไม่แสดง (หรือในล็อก) และให้ข้อความผู้ใช้ที่ปลอดภัยและปฏิบัติได้
เปลี่ยนความล้มเหลวของแบ็กเอนด์เป็นคำแนะนำที่ผู้ใช้ทำตามได้
ข้อผิดพลาดส่วนใหญ่มีประโยชน์กับวิศวกร แต่บ่อยครั้งมันไปปรากฏบนหน้าจอเป็น "Something went wrong" สัญญาข้อผิดพลาดที่ดีเปลี่ยนความล้มเหลวให้เป็นขั้นตอนถัดไปที่ชัดเจนโดยไม่รั่วไหลข้อมูลสำคัญ
แนวทางง่ายๆ คือแม็พความล้มเหลวเป็นหนึ่งในสามการกระทำสำหรับผู้ใช้: แก้ข้อมูล, ลองใหม่, หรือติดต่อซัพพอร์ต วิธีนี้ทำให้ UI สม่ำเสมอทั้งเว็บและมือถือแม้แบ็กเอนด์จะมีโหมดล้มเหลวมากมาย
- แก้ข้อมูล: การตรวจสอบความถูกต้องล้มเหลว รูปแบบผิด ขาดฟิลด์ที่จำเป็น
- ลองใหม่: timeout ปัญหาชั่วคราวจาก upstream ขีดจำกัดอัตรา
- ติดต่อซัพพอร์ต: ปัญหาสิทธิ์ ความขัดแย้งที่ผู้ใช้แก้ไม่ได้ หรือข้อผิดพลาดภายในที่ไม่คาดคิด
คำแนะนำฟิลด์สำคัญกว่าข้อความยาว เมื่อแบ็กเอนด์รู้ว่าฟิลด์ไหนล้มเหลว ให้คืน pointer ที่อ่านด้วยเครื่อง (เช่น ชื่อฟิลด์ email หรือ card_number) และเหตุผลสั้นๆ ที่ UI จะแสดงแบบอินไลน์ หากมีหลายฟิลด์ผิด ให้คืนทั้งหมดเพื่อให้ผู้ใช้แก้ได้ในครั้งเดียว
ยังช่วยถ้าจับคู่รูปแบบ UI กับสถานการณ์ด้วย toast เหมาะกับข้อความลองใหม่ชั่วคราว ข้อผิดพลาดอินพุตควรแสดงแบบอินไลน์ ปัญหาบัญชีหรือการชำระเงินมักต้องใช้ไดอะล็อกบล็อกการทำงาน
รวมบริบทการแก้ปัญหาอย่างปลอดภัยอย่างสม่ำเสมอ: trace_id, timestamp ถ้ามี และขั้นตอนแนะนำเช่นเวลาที่ควรลองใหม่ ด้วยวิธีนี้ timeout ของผู้ให้บริการการชำระเงินจะแสดง "บริการการชำระเงินช้า กรุณาลองอีกครั้ง" พร้อมปุ่มลองใหม่ ขณะที่ฝ่ายซัพพอร์ตใช้ trace_id เดียวกันค้นหาความล้มเหลวฝั่งเซิร์ฟเวอร์ได้
ขั้นตอนทีละขั้น: นำสัญญาไปใช้ทั่วทั้งระบบ
การนำสัญญาข้อผิดพลาดของ API ไปใช้ดีที่สุดเมื่อถือเป็นการเปลี่ยนแปลงผลิตภัณฑ์ขนาดเล็ก ไม่ใช่แค่รีแฟกเตอร์ ทำแบบค่อยเป็นค่อยไป และให้ทีมซัพพอร์ตกับ UI มีส่วนร่วมตั้งแต่ต้น
ลำดับการนำไปใช้ที่ช่วยปรับปรุงข้อความผู้ใช้ได้เร็วโดยไม่ทำให้ไคลเอ็นต์พัง:
- สำรวจสิ่งที่มีอยู่ (จัดกลุ่มตามโดเมน) ส่งออกการตอบข้อผิดพลาดจริงจากล็อกและจัดกลุ่มตามโดเมน เช่น auth, signup, billing, file upload, permissions มองหาการซ้ำ ข้อความไม่ชัด และสถานที่ที่ความล้มเหลวเหมือนกันมีหลายรูปแบบ
- กำหนดสคีมาและแชร์ตัวอย่าง เอกสารรูปร่างการตอบกลับ ฟิลด์ที่ต้องมี และตัวอย่างต่อโดเมน รวมรหัสคงที่ คีย์ข้อความสำหรับแปล และส่วนคำแนะนำสำหรับ UI (ถ้ามี)
- ลงมือทำตัวแม็พข้อผิดพลาดศูนย์กลาง ใส่การฟอร์แมตไว้ที่เดียวเพื่อให้ทุก endpoint คืนโครงสร้างเดียวกัน ในแบ็กเอนด์ที่สร้างโดยอัตโนมัติ (หรือแบ็กเอนด์แบบ no-code อย่าง AppMaster) มักหมายถึงขั้นตอนเดียวที่แชร์ว่า "แม็พข้อผิดพลาดเป็นการตอบกลับ" ที่ทุก endpoint หรือกระบวนการเรียก
- อัปเดต UI ให้ตีความรหัสและแสดงคำแนะนำ ให้ UI พึ่งพารหัส ไม่ใช่ข้อความ ใช้รหัสตัดสินใจว่าจะเน้นฟิลด์ แสดงการกระทำลองใหม่ หรืแนะนำการติดต่อซัพพอร์ต
- เพิ่มการล็อกพร้อม trace_id ที่ซัพพอร์ตขอได้ สร้าง trace_id สำหรับทุกคำขอ บันทึกลงล็อกฝั่งเซิร์ฟเวอร์พร้อมรายละเอียดความล้มเหลวดิบ และคืนให้ในการตอบข้อผิดพลาดเพื่อให้ผู้ใช้คัดลอกได้
หลังการรอบแรก รักษาสัญญาให้เสถียรด้วยเอกสารน้ำหนักเบา: พจนานุกรมรหัสข้อผิดพลาดต่อโดเมน ไฟล์แปลสำหรับข้อความที่แสดงให้ผู้ใช้ ตารางแม็พจากรหัส -> คำแนะนำ UI/การกระทำถัดไป และ playbook ฝ่ายซัพพอร์ตที่เริ่มจาก "ส่ง trace_id มาให้เรา"
หากมีไคลเอ็นต์เก่ารักษาฟิลด์เก่าไว้ชั่วระยะเวลาการเลิกใช้ แต่หยุดสร้างรูปแบบหนึ่ง-off ใหม่ทันที
ความผิดพลาดทั่วไปที่ทำให้การซัพพอร์ตยากขึ้น
ปัญหาส่วนใหญ่ของฝ่ายซัพพอร์ตไม่ได้มาจาก "ผู้ใช้แย่" แต่มาจากความกำกวม เมื่อสัญญาข้อผิดพลาดไม่สอดคล้อง ทุกทีมก็คิดเองทำเอง และผู้ใช้เจอข้อความที่ทำอะไรไม่ได้
กับดักที่พบบ่อยคือการถือว่า HTTP status code คือเรื่องทั้งหมด "400" หรือ "500" บอกอะไรผู้ใช้ได้น้อยมาก รหัสสถานะช่วยเรื่องการขนส่งและการจำแนกกว้าง แต่คุณยังต้องการรหัสระดับแอปที่มีความหมายคงที่ข้ามเวอร์ชัน
ข้อผิดพลาดอีกอย่างคือการเปลี่ยนความหมายของรหัสตามเวลา หาก PAYMENT_FAILED เคยหมายถึง "บัตรถูกปฏิเสธ" แล้วต่อมาหมายถึง "Stripe ล่ม" UI และเอกสารจะผิดโดยไม่มีใครสังเกต ฝ่ายซัพพอร์ตจึงได้ตั๋วเช่น "ลองบัตรสามใบแล้วยังล้ม" ในขณะที่ปัญหาจริงคือการล่มของผู้ให้บริการ
การคืนข้อความ exception ดิบ (หรือยิ่งกว่านั้น สแตกเทรซ) ก็ล่อตาล่อใจเพราะทำเร็ว แต่มักไม่เป็นประโยชน์กับผู้ใช้และอาจรั่วไหลข้อมูลภายใน เก็บดีบักดิบในล็อก อย่าใส่ในการตอบกลับที่แสดงต่อผู้ใช้
รูปแบบที่สร้างเสียงรบกวนได้บ่อย:
- ใช้รหัสจับหมดเช่น
UNKNOWN_ERRORบ่อยไป จะทำให้ไม่สามารถแนะนำผู้ใช้ได้ - สร้างรหัสมากเกินไปโดยไม่มีระบบจัดหมวดหมู่ ทำให้แดชบอร์ดและ playbook ยากดูแล
- ผสมข้อความสำหรับผู้ใช้กับข้อมูลดีบักในฟิลด์เดียวกัน ทำให้การแปลและคำแนะนำ UI เปราะบาง
กฎง่ายๆ: รหัสหนึ่งรหัสต่อการตัดสินใจของผู้ใช้ หากผู้ใช้แก้ได้โดยเปลี่ยนอินพุต ให้ใช้รหัสเฉพาะพร้อมคำแนะนำชัดเจน ถ้าแก้ไม่ได้ (เช่น การล่มของผู้ให้บริการ) ให้รักษารหัสเสถียรและคืนข้อความปลอดภัยพร้อมการกระทำเช่น "ลองอีกครั้งทีหลัง" และ correlation ID สำหรับซัพพอร์ต
เช็คลิสต์ก่อนปล่อย
ก่อนส่ง ให้ถือว่าข้อผิดพลาดเป็นฟีเจอร์ผลิตภัณฑ์ เมื่อเกิดความล้มเหลว ผู้ใช้ควรรู้ว่าต้องทำอะไรต่อ ฝ่ายซัพพอร์ตควรหาเหตุการณ์ที่แน่นอนได้ และไคลเอ็นต์ไม่ควรพังเมื่อแบ็กเอนด์เปลี่ยน
- รูปร่างเดียวกันทุกที่: ทุก endpoint (รวม auth, webhooks, file uploads) คืนซองข้อผิดพลาดเดียวกัน
- รหัสที่เสถียรและมีเจ้าของ: แต่ละรหัสมีเจ้าของชัดเจน (Payments, Auth, Billing) อย่าใช้รหัสซ้ำกับความหมายอื่น
- ข้อความปลอดภัยและแปลได้: ข้อความที่แสดงสั้นและไม่รวมความลับ (โทเคน ข้อมูลบัตรเต็ม ข้อความ SQL ดิบ สแตกเทรซ)
- คำแนะนำ UI ที่ชัดเจน: สำหรับประเภทความล้มเหลวยอดนิยม UI แสดงการกระทำถัดไปหนึ่งอย่างที่ชัดเจน (ลองอีกครั้ง อัปเดตฟิลด์ ใช้วิธีการชำระเงินอื่น ติดต่อซัพพอร์ต)
- ติดตามได้สำหรับซัพพอร์ต: ทุกการตอบข้อผิดพลาดมี
trace_id(หรือเทียบเท่า) ที่ซัพพอร์ตขอได้ และระบบล็อก/มอนิเตอร์หาข้อมูลเต็มได้อย่างรวดเร็ว
ทดสอบฟลว์สมจริงจบหนึ่งรอบ: ฟอร์มที่มีอินพุตไม่ถูกต้อง เซสชันหมดอายุ ขีดจำกัดอัตรา และการล้มของ third-party หากคุณอธิบายความล้มเหลวได้ในหนึ่งประโยคและชี้ไปยัง trace_id ในล็อก คุณก็พร้อมปล่อย
ตัวอย่าง: กรณีสมัครและการชำระเงินที่ผู้ใช้กู้คืนได้
สัญญาข้อผิดพลาดที่ดีทำให้ความล้มเหลวเดียวกันเข้าใจได้ในสามที่: เว็บ UI ของคุณ แอปมือถือ และอีเมลอัตโนมัติที่ระบบอาจส่งหลังความพยายามล้มเหลว มันยังให้ข้อมูลเพียงพอแก่ฝ่ายซัพพอร์ตโดยไม่ต้องให้ผู้ใช้ถ่ายภาพหน้าจอทุกอย่าง
สมัคร: ข้อผิดพลาดการตรวจสอบที่ผู้ใช้แก้ได้
ผู้ใช้ป้อนอีเมลอย่าง sam@ แล้วกดสมัคร API คืนรหัสเสถียรและคำแนะนำระดับฟิลด์ ดังนั้นไคลเอ็นต์ทุกตัวจะไฮไลต์อินพุตเดียวกัน
{
"error": {
"code": "AUTH.EMAIL_INVALID",
"message": "Enter a valid email address.",
"i18n_key": "auth.email_invalid",
"params": { "field": "email" },
"ui": { "field": "email", "action": "focus" },
"trace_id": "4f2c1d..."
}
}
บนเว็บ แสดงข้อความใต้ช่องอีเมล บนมือถือ โฟกัสฟิลด์อีเมลและโชว์แบนเนอร์เล็กๆ ในอีเมลคุณสามารถบอกว่า: "เราไม่สามารถสร้างบัญชีได้เพราะที่อยู่อีเมลไม่สมบูรณ์" รหัสเดียว ความหมายเดียวกัน
การชำระเงิน: ล้มเหลวพร้อมคำอธิบายที่ปลอดภัย
การชำระเงินด้วยบัตรล้มเหลว ผู้ใช้ต้องการคำแนะนำ แต่คุณไม่ควรเปิดเผยรายละเอียดภายใน สัญญาของคุณจึงแยกสิ่งที่ผู้ใช้เห็นออกจากสิ่งที่ฝ่ายซัพพอร์ตตรวจสอบได้
{
"error": {
"code": "PAYMENT.DECLINED",
"message": "Your payment was declined. Try another card or contact your bank.",
"i18n_key": "payment.declined",
"params": { "retry_after_sec": 0 },
"ui": { "action": "show_payment_methods" },
"trace_id": "b9a0e3..."
}
}
ฝ่ายซัพพอร์ตขอ trace_id แล้วตรวจสอบว่าถูกคืนรหัสใด เป็น decline แบบถาวรหรือลองใหม่ได้ เกี่ยวกับบัญชีและจำนวนเงินที่พยายาม และว่า UI ได้รับคำแนะนำหรือไม่
นี่คือจุดที่สัญญาข้อผิดพลาดของ API ให้ผลตอบแทน: เว็บ iOS/Android และอีเมลของคุณคงความสอดคล้อง แม้ผู้ให้บริการภายนอกหรือรายละเอียดภายในจะเปลี่ยน
การทดสอบและมอนิเตอร์สัญญาตลอดเวลา
สัญญาข้อผิดพลาดของ API ไม่ได้ "เสร็จ" เมื่อปล่อย มันเสร็จเมื่อรหัสข้อผิดพลาดเดียวกันยังคงนำไปสู่การกระทำผู้ใช้เหมือนเดิมแม้จะผ่านการรีแฟกเตอร์และฟีเจอร์ใหม่หลายเดือน
เริ่มจากทดสอบจากภายนอก เหมือนไคลเอ็นต์จริง สำหรับรหัสข้อผิดพลาดแต่ละรายการ เขียนคำขอที่กระตุ้นมันอย่างน้อยหนึ่งรายการและยืนยันพฤติกรรมที่คุณพึ่งพาจริงๆ: สถานะ HTTP, รหัส, คีย์แปล, และฟิลด์คำแนะนำ UI (เช่น ฟิลด์ไหนจะไฮไลต์)
ชุดทดสอบเล็กๆ ครอบคลุมความเสี่ยงส่วนใหญ่:
- คำขอแนวทางสำเร็จหนึ่งรายการถัดจากแต่ละเคสข้อผิดพลาด (จับการตรวจสอบที่มากเกินโดยไม่ได้ตั้งใจ)
- เทสต์หนึ่งรายการต่อรหัสเสถียรเพื่อตรวจฟิลด์คำแนะนำหรือการแม็พฟิลด์
- เทสต์หนึ่งรายการเพื่อให้แน่ใจว่าความล้มเหลวไม่ทราบขึ้นมาจะคืนรหัสทั่วไปที่ปลอดภัย
- เทสต์หนึ่งรายการเพื่อให้คีย์แปลมีอยู่สำหรับแต่ละภาษาที่รองรับ
- เทสต์หนึ่งรายการเพื่อให้แน่ใจว่ารายละเอียดลับไม่ปรากฏในการตอบกลับของไคลเอ็นต์
มอนิเตอร์ช่วยจับการถดถอยที่เทสต์พลาด ติดตามจำนวนของรหัสข้อผิดพลาดตามเวลาและเตือนเมื่อมีพุ่งขึ้นอย่างฉับพลัน (เช่น รหัสการชำระเงินเพิ่มเป็นสองเท่าหลังรีลีส) และจับตาดูรหัสใหม่ในโปรดักชัน ถ้ามีรหัสปรากฏที่ไม่อยู่ในรายการเอกสาร แสดงว่าใครบางคนอาจข้ามสัญญา
ตัดสินใจแต่เนิ่นๆว่าสิ่งใดเก็บภายในกับสิ่งใดส่งให้ไคลเอ็นต์ แยกปฏิบัติได้ง่ายคือ: ไคลเอ็นต์ได้รหัสเสถียร คีย์แปล และคำแนะนำการกระทำ; ล็อกได้สแตกเทรซ ไอดีคำขอ และความล้มเหลวของ dependency
ครั้งละเดือน รีวิวข้อผิดพลาดโดยใช้การสนทนาจากซัพพอร์ตจริง เลือกรหัสห้าอันดับแรกตามปริมาณและอ่านตั๋วหรือแชทไม่กี่รายการสำหรับแต่ละรหัส หากผู้ใช้ยังถามคำถามเดิมซ้ำ แสดงว่าคำแนะนำ UI ขาดขั้นตอน หรือข้อความยังคลุมเครือ
ขั้นตอนถัดไป: นำรูปแบบไปใช้ในผลิตภัณฑ์และเวิร์กโฟลว์ของคุณ
เริ่มจากจุดที่ความสับสนมีต้นทุนสูงที่สุด: ขั้นตอนที่มีการทิ้งบ่อยที่สุด (มักเป็นการสมัคร ชำระเงิน หรืออัปโหลดไฟล์) และข้อผิดพลาดที่สร้างตั๋วมากที่สุด มาตรฐานเหล่านี้ก่อนเพื่อเห็นผลภายในสปรินท์
วิธีปฏิบัติที่ช่วยโฟกัสการนำไปใช้:
- เลือกรหัสข้อผิดพลาด 10 อันดับแรกที่ขับเคลื่อนซัพพอร์ตและกำหนดรหัสเสถียรและค่าดีฟอลต์
- นิยามการแม็พรหัส -> คำแนะนำ UI -> การกระทำถัดไป ต่อแต่ละ surface (เว็บ มือถือ แอดมิน)
- ทำให้สัญญาเป็นค่าพื้นฐานสำหรับ endpoint ใหม่และมองว่าการขาดฟิลด์เป็นความล้มเหลวการตรวจทาน
- เก็บ playbook ภายในเล็กๆ: แต่ละรหัสหมายถึงอะไร ซัพพอร์ตขออะไร และใครเป็นเจ้าของการแก้ไข
- ติดตามเมตริกบางรายการ: อัตราข้อผิดพลาดตามรหัส, จำนวน "unknown error", และปริมาณตั๋วที่ผูกกับแต่ละรหัส
ถ้าคุณสร้างด้วย AppMaster (appmaster.io) การฝังสิ่งนี้ตั้งแต่ต้นคุ้มค่า: กำหนดรูปร่างข้อผิดพลาดที่สอดคล้องสำหรับ endpoint ของคุณ แล้วแม็พรหัสเสถียรไปยังข้อความ UI ในหน้าจอเว็บและมือถือของคุณเพื่อให้ผู้ใช้ได้รับความหมายเดิมทุกที่
ตัวอย่างง่ายๆ: ถ้าซัพพอร์ตได้รับเรื่องร้องเรียน "Payment failed" ซ้ำ ๆ มาตรฐานจะทำให้ UI แสดง "Card declined" พร้อมคำแนะนำให้ลองบัตรอื่นสำหรับรหัสหนึ่ง และ "Payment system temporarily unavailable" พร้อมการกระทำลองใหม่สำหรับอีกรหัส ฝ่ายซัพพอร์ตขอ trace_id แทนที่จะเดา
ตั้งการทำความสะอาดซ้ำๆ ในปฏิทิน เลิกใช้รหัสที่ไม่ได้ใช้ ปรับข้อความคลุมเครือ และเพิ่มข้อความแปลเมื่อมีปริมาณการใช้งานจริง สัญญายังคงเสถียรในขณะที่ผลิตภัณฑ์เปลี่ยนแปลงต่อไป


