ExpressJS, How to respond when streaming fails
- Sathit Jittanupat
- 7 มิ.ย. 2566
- ยาว 1 นาที

การออกแบบ Backend Server ที่ใช้ติดต่อกับ Database มีข้อควรระวังตรงที่ขนาดของข้อมูลที่อ่านมาได้ก่อนที่จะส่งไปให้ Client จะถูกจำกัดด้วยขนาดของหน่วยความจำของ Server เพราะข้อมูลที่อ่านมาได้จาก Database ต้องถูกพักใน buffer หรือหน่วยความจำก่อนที่จะแปลงเป็น payload ตาม format ที่กำหนด เช่น JSON
ขณะเดียวกันก็ต้องจินตนาการด้วยว่า Server นั้นอาจต้องให้บริการ client พร้อม ๆ กัน หากบังเอิญเป็นจังหวะที่อ่านข้อมูลขนาดใหญ่พร้อมกัน อาจมีผลให้หน่วยความจำไม่เพียงพอ และกลายเป็นสาเหตุที่ทำให้หยุดทำงาน
เทคนิคการทำ Database API ที่ให้บริการข้อมูล โดยไม่ต้องใช้หน่วยความจำมาก ๆ เผื่อไว้สำหรับข้อมูลขนาดใหญ่ สามารถใช้วิธี streaming เช่นเดียวกับการส่งข้อมูลไฟล์วิดีโอที่มีขนาดใหญ่ และเป็นกระบวนท่าที่แนะนำให้ทำด้วย

เรื่องนี้ผมเคยเล่าเรื่องการเขียนโค้ดเพื่อ stream ข้อมูลแบบ JSON เมื่อ 4 ปีที่แล้ว และใช้มาตลอด ทำให้ไม่ต้องสิ้นเปลืองสเปคของ Server หน่วยความจำขนาด 256 MB ก็เอาอยู่ สามารถรองรับ 80 concurrent requests ได้สบาย ๆ มีข้อที่ควรระวังอยู่ตรงที่บริการ Cloud บางประเภทไม่สามารถส่งข้อมูลแบบ stream ได้ เช่น App Engine, Cloud Function (gen 1)
ยังมีปัญหาที่นาน ๆ ครั้ง server จะ restart ซึ่งผมเพิ่งมีเวลาได้ตรวจสอบ log และสืบหาสาเหตุอย่างจริงจัง แล้วก็พบว่าโค้ดที่เขียนไว้เดิมสำหรับ stream JSON นั้นยังมีจุดบกพร่องอยู่ ทำให้เกิด error “ERR_HTTP_HEADERS_SENT”

สาเหตุของปัญหาข้างต้น เมื่อเข้าใจแล้วก็อธิบายได้ง่าย ๆ การ stream ข้อมูลนั้นเป็น optimistic มองโลกในแง่ดีว่าการส่งข้อมูลนั้นสุดท้ายต้องสำเร็จเรียบร้อย (http status 200) แล้วถ้าระหว่างที่ส่งข้อมูลไปบางส่วนแล้วเกิดปัญหา เช่น database connection error ทำให้ไม่การส่งไม่สำเร็จจะต้องทำอย่างไร ?
ปกติการเขียนโค้ดฝั่ง Server หากเกิด error จะมีมาตรฐาน http status code สำหรับตอบกลับ เช่น 500 หรือ 400 เพื่อให้ฝั่ง client ทราบว่าเกิด error ซึ่งกรณีนี้ผมก็ส่ง status 500 ไปให้ แต่กลายเป็นว่าการทำเช่นนั้นเป็นวิธีที่ไม่ถูกต้อง และมีผลทำให้ crash จน restart
เมื่อเริ่มต้น stream ได้ส่ง status 200 ไปแล้ว ดังนั้นจึงไม่สามารถส่ง status 500 ออกไปอีกเมื่อเกิด error วิธีที่ถูกต้องในการ response เมื่อเกิด error ขึ้นกลางคัน คือ ตัดจบดื้อ ๆ ปล่อยให้ฝั่ง client ตรวจเองว่าข้อมูลที่ได้รับนั้นไม่สมบูรณ์ สำหรับ JSON คือ ไม่มีวงเล็บปิด เป็นข้อมูลที่ไม่สมบูรณ์เกิด loss ระหว่างทาง
ตัวอย่างโค้ด ใช้วิธีตรวจ res.headersSent ซึ่งหมายความว่ามีการส่ง http status 200 ไปแล้ว หากไม่ใช่ก็สามารถส่ง status 500 error ได้ตามเดิม
const errorHandler = (err, dbname, colname, res) => {
const emsg = eMessage(err, `${dbname}.${colname}`)
_showError(emsg)
if (res.headersSent)
res.end()
else
res.status(eStatus(err) || 500).json({message: emsg})
}const responseError = (err) => {
return errorHandler(err, dbname, colname, res, 'Find')
}
...
cursor.stream()
.on('error', responseError)
.pipe(streamDocToJson())
.on('error', responseError)
.pipe(res.type('json'))
.on('error', responseError)เป็นเคสที่หาข้อมูลไม่ค่อยได้ ต้องปะติดปะต่อแล้วพยายามทำความเข้าใจเรื่อง stream ของ Node.js กับ framework Express จึงบันทึกเก็บไว้กันลืม และเผื่อจะเป็นประโยชน์สำหรับผู้ที่ต้องทำ Backend Database API
Node.js stream https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
Stack overflow https://stackoverflow.com/questions/12030107/express-js-how-to-check-if-headers-have-already-been-sent/24077691#24077691
MongoDB — API response with stream json https://jsat66.medium.com/mongodb-api-response-with-stream-json-bb1a76a88f94



ความคิดเห็น