Error Extensions
message มีไว้ให้มนุษย์อ่าน log แต่ client ไม่สามารถแตกกิ่งโปรแกรมบนประโยคได้อย่างน่าเชื่อถือ เพราะสตริงถูกเรียบเรียงใหม่ ถูกแปล และถูกจัดรูปแบบใหม่ได้ เพื่อให้ client ตอบสนองต่อความล้มเหลวในเชิงโปรแกรมได้ GraphQL จึงให้ทุก error มี extensions map ที่เป็น object อิสระที่ server กำหนดเอง ห้อยอยู่กับ error entry นี่คือที่ที่ typed errors อาศัยอยู่
extensions map
หัวข้อที่มีชื่อว่า “extensions map”extensions คือ key เดียวของ error ที่ specification เปิดทิ้งไว้ให้คุณเติมเอง ธรรมเนียมที่แทบจะเป็นสากลคือใส่สตริง code ที่คงที่ลงไปในนั้น
{ "errors": [ { "message": "No track exists with that id.", "path": ["track"], "extensions": { "code": "NOT_FOUND" } } ]}message อาจเปลี่ยนไปตามกาลเวลา แต่ extensions.code คือ contract เช่น "NOT_FOUND", "UNAUTHENTICATED", "FORBIDDEN", "BAD_USER_INPUT" client switch บน code ไม่ใช่บนตัวประโยค คุณยังแนบอะไรก็ตามที่คุณเห็นว่ามีประโยชน์ได้ด้วย ไม่ว่าจะเป็นคำใบ้ให้ลองใหม่ ชื่อ field หรือ correlation id ตราบใดที่ปลอดภัยพอจะเปิดเผยออกไป
การ throw typed error
หัวข้อที่มีชื่อว่า “การ throw typed error”reference implementation export class GraphQLError มาให้ throw instance ของ class นี้จาก resolver แล้วส่ง extensions เข้าไปใน options จากนั้น engine จะร้อยเข้า response ให้เอง
import { GraphQLError } from 'graphql';
throw new GraphQLError('No track exists with that id.', { extensions: { code: 'NOT_FOUND' },});ลองเทียบกับการ throw Error ธรรมดาดู Error ธรรมดาก็ให้ error entry ที่ถูกต้องได้เหมือนกัน อย่างที่เห็นในบทก่อน ๆ แต่ไม่มี code ติดมา client จึงต้องไปแกะ message เอาเอง การเลือกใช้ GraphQLError คือวิธีทำให้ความล้มเหลวเป็นส่วนหนึ่งของ typed contract แทนที่จะขึ้นอยู่กับถ้อยคำโดยบังเอิญ
การปกปิด internal errors
หัวข้อที่มีชื่อว่า “การปกปิด internal errors”มีแง่มุมด้านความปลอดภัยด้วย exception ที่ไม่คาดคิด เช่น การเรียก database ที่ล้มเหลว หรือการ dereference null ลึก ๆ ใน code ของคุณ มักพก stack trace หรือข้อความภายในที่ห้ามส่งไปให้ client เด็ดขาด รูปแบบเชิงป้องกันคือ ปล่อยให้ความล้มเหลวที่ คาดไว้ throw GraphQLError พร้อม code ที่ตั้งใจ แล้ว ดักทุกอย่างที่เหลือ เปลี่ยนเป็น masked error กลาง ๆ ก่อนหลุดออกจาก server
flowchart TD
Throw["Resolver throws"]
Check{"Is it a deliberate GraphQLError?"}
Keep["Expected: keep message + code (NOT_FOUND, FORBIDDEN, ...)"]
Mask["Unexpected: log server-side, return generic INTERNAL_SERVER_ERROR"]
Client["Client reads extensions.code"]
Throw --> Check
Check -->|"yes"| Keep
Check -->|"no"| Mask
Keep --> Client
Mask --> Client ใน production โดยทั่วไปคุณจะ log exception จริงไว้ฝั่ง server (พร้อม correlation id) และ return ให้ client เพียง { message: "Internal server error", extensions: { code: "INTERNAL_SERVER_ERROR" } } ที่จืดชืด ผู้ใช้ได้ข้อมูลพอที่จะลองใหม่หรือรายงาน ส่วนผู้โจมตีไม่ได้เรียนรู้อะไรเกี่ยวกับภายในของคุณเลย GraphQL server หลายตัวปกปิดให้เป็นค่าเริ่มต้นอยู่แล้ว หลักการเหมือนกันไม่ว่าจะเป็น framework หรือ catch block ของคุณเองที่ทำ
typed error ที่รันจริง
หัวข้อที่มีชื่อว่า “typed error ที่รันจริง”runner ด้านล่าง throw GraphQLError พร้อม extensions: { code: 'NOT_FOUND' } เมื่อหา track ไม่เจอ กด Run แล้วดู error entry จะเห็น code ของคุณวางอยู่ข้าง ๆ message และ path เลย
ใน output นั้น data.track เป็น null เพราะ field เป็น nullable และ error entry เพียงตัวเดียวตอนนี้พก extensions.code === "NOT_FOUND" พร้อมคำใบ้ argument ที่เราแนบไว้ client สามารถอ่าน code นั้นและแสดงผลว่า “track not found” ได้โดยไม่ต้องไปแกะข้อความภาษาอังกฤษเลย ที่เป็น contract เชิงโปรแกรมที่ Error ธรรมดาให้เราไม่ได้
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| error code ทำให้ client จัดการ error programmatically | extensions ที่มากเกินทำให้ error response ใหญ่ |
| timestamp, requestId ใน extensions ช่วย debug | sensitive information ใน extensions อาจ leak ได้ |
| custom extension ให้ข้อมูล domain-specific error | format extensions ไม่มีมาตรฐาน — ต้องตกลงกันเอง |
| stack trace ใน extensions ช่วย development | stack trace ใน production เป็น security risk |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Stack Trace ใน Production Response
อาการ:
- extensions มี
stacktracearray แม้ใน production - expose internal code path และ file structure ให้ผู้โจมตี
- ปิด stack trace ใน production:
formatErrorที่ filter out stacktrace เมื่อNODE_ENV === 'production'
Error Code ที่ไม่ Documented
อาการ:
- client ได้
"code": "ERR_42"แต่ไม่รู้ว่าหมายถึงอะไร - developer ต้องถามหรือดู source code เพื่อรู้ error code
- document error code ทั้งหมดใน schema description หรือ API doc
💡 ตัวอย่างจากของจริง
Apollo Server:
- error extensions มี
code(enum) และhttp.statusสำหรับ custom HTTP status- production build ซ่อน
stacktraceอัตโนมัติStripe:
- error code เป็น machine-readable string:
card_declined,insufficient_funds- client จัดการแต่ละ error code ต่างกันโดยไม่ต้อง parse error message