ส่งต่อ Webhook ไปยังระบบลูกค้า
ภาพรวม
ลูกค้าบางรายมีระบบของตัวเองที่ต้องการรับ LINE webhook ด้วย แต่ LINE อนุญาตให้ตั้ง webhook URL ได้เพียง 1 ปลายทางต่อ 1 OA เท่านั้น ฟีเจอร์นี้จึงถูกออกแบบมาแก้ปัญหาดังกล่าว โดยให้ระบบของเราเป็นปลายทางเดียวที่ LINE รู้จัก แล้ว คัดลอก request เดิมทั้งชุด ยิงต่อไปยัง endpoint ของลูกค้า
จุดเด่นคือลูกค้ายังคง verify x-line-signature ได้เสมือนรับตรงจาก LINE
เพราะระบบส่ง bytes ดิบพร้อม header ลายเซ็นเดิมไปให้ครบ
(ดู การส่งต่อ Raw Body และลายเซ็น)
การส่งต่อทำงานแบบ fire-and-forget สองชั้น กล่าวคือ pipeline หลักไม่รอผล และตัว forward เองก็ spawn goroutine แยกอีกชั้นหนึ่ง ดังนั้นหากปลายทางล่ม จะไม่กระทบ flow ปกติของระบบเราเลย
Business Flow
เส้นทางที่ 1 — forward ทั่วไป (ทุก event)
ProcessLineอ่านwebhook_configแล้วพบว่าฟิลด์forwardWebhookUrlไม่ว่าง- เรียก
forwardWebhookพร้อม URL, header, body และ bytes ดิบ โดยไม่รอผล - pipeline หลักเดินหน้าต่อตามปกติ ยังคงเข้าสู่ mbox routing หรือ publish ลงคิวเช่นเดิม การ forward จึงไม่ได้แทนที่การประมวลผลปกติ แต่เป็นการทำงาน คู่ขนาน
เส้นทางที่ 2 — forward ขณะอยู่ใน agent mode
- เมื่อผู้ใช้อยู่ใน agent mode และข้อความไม่ใช่ exit keyword ระบบจะ forward
ไปที่
mboxLineWebhookUrlแทน เพื่อให้ระบบ mbox รับไปแสดงในหน้าจอของเจ้าหน้าที่ (ดู ส่งต่อให้เจ้าหน้าที่)
รายละเอียดการยิง request
- parse URL ก่อนเสมอ หาก parse ไม่ผ่านจะ log warn
Forward webhook failed: invalid urlแล้วยกเลิกการส่ง - spawn goroutine ยิง
POSTไปยัง URL ปลายทางด้วยcontext.WithTimeoutที่ 5 วินาที - body ที่ส่งคือ
BodyRawซึ่งเป็น bytes เดิม หากไม่มี raw จึงค่อย marshal จาก map - header ที่ส่งไปมีเพียง
content-type: application/jsonและx-line-signature(หากมีในต้นทาง) โดย ไม่ได้ forward header อื่นเลย ไม่มีทั้ง user-agent และ x-forwarded-for - เซ็ต
req.Hostให้เป็น host ของ URL ปลายทาง - response ถูก drain ทิ้งด้วย
io.Copy(io.Discard)แล้วปิด connection โดย ไม่สนใจ status code หากปลายทางตอบ 500 ก็จะไม่มี log ไม่มี retry และไม่มี dead letter queue - error ระดับ network จะ log warn เพียงอย่างเดียว
:::warning ข้อจำกัดที่ควรทราบ กลไกนี้ไม่มี retry ไม่มี circuit breaker และไม่เก็บสถิติใด ๆ หากระบบปลายทางของลูกค้าล่ม event ในช่วงเวลานั้นจะหายไปเงียบ ๆ และตรวจสอบย้อนหลังได้จาก log warn เท่านั้น :::
ไฟล์และฟังก์ชันหลัก
| ไฟล์ | ฟังก์ชัน |
|---|---|
internal/line/service.go | Service.forwardWebhook(rawURL, headers, body, raw) เป็นตัวหลัก |
internal/line/service.go | Service.ProcessLine เป็นจุดเรียกทั้ง 2 เส้นทาง ผ่านค่า forwardWebhookUrl และ mCfg.LineWebhookURL |
internal/line/service.go | interface httpDoer ซึ่ง *http.Client implement อยู่แล้ว ทำให้ mock ได้ใน test |
HTTP client ที่ใช้เป็น &http.Client{} เปล่า ๆ ที่สร้างขึ้นใน line.Register
โดยไม่ได้ใช้ internal/httpclient ของ template ที่มี retry และ backfoff ในตัว
การควบคุมเวลาจึงใช้ context timeout แทน
จุดเชื่อมต่อกับ Service อื่น
- Redis — ค่า
forwardWebhookUrlและmboxLineWebhookUrlมาจาก hashwebhook_config(ดู Redis Cache ของ Webhook Config) - cms-api — หน้าจัดการ LINE OA ในโมดูล
line-oa-managementเป็นที่ตั้งค่า URL นี้ ซึ่งตรงกับคอลัมน์line_oa.forward_webhook_urlในฐานข้อมูล - คิว
line_forward_webhook— ถูกประกาศไว้ใน topology ผ่านตัวแปรRABBITMQ_QUEUE_LINE_FORWARD_WEBHOOKแต่ service นี้ไม่เคย publish ลงไปเลย เพราะการ forward ทำผ่าน HTTP โดยตรงไม่ผ่านคิว คิวดังกล่าวน่าจะเป็นของเดิม หรือถูกใช้โดย worker-go - ไม่มี dependency ต่อ PostgreSQL