Skip to main content

ส่งต่อ 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)

  1. ProcessLine อ่าน webhook_config แล้วพบว่าฟิลด์ forwardWebhookUrl ไม่ว่าง
  2. เรียก forwardWebhook พร้อม URL, header, body และ bytes ดิบ โดยไม่รอผล
  3. pipeline หลักเดินหน้าต่อตามปกติ ยังคงเข้าสู่ mbox routing หรือ publish ลงคิวเช่นเดิม การ forward จึงไม่ได้แทนที่การประมวลผลปกติ แต่เป็นการทำงาน คู่ขนาน

เส้นทางที่ 2 — forward ขณะอยู่ใน agent mode

  1. เมื่อผู้ใช้อยู่ใน agent mode และข้อความไม่ใช่ exit keyword ระบบจะ forward ไปที่ mboxLineWebhookUrl แทน เพื่อให้ระบบ mbox รับไปแสดงในหน้าจอของเจ้าหน้าที่ (ดู ส่งต่อให้เจ้าหน้าที่)

รายละเอียดการยิง request

  1. parse URL ก่อนเสมอ หาก parse ไม่ผ่านจะ log warn Forward webhook failed: invalid url แล้วยกเลิกการส่ง
  2. spawn goroutine ยิง POST ไปยัง URL ปลายทางด้วย context.WithTimeout ที่ 5 วินาที
  3. body ที่ส่งคือ BodyRaw ซึ่งเป็น bytes เดิม หากไม่มี raw จึงค่อย marshal จาก map
  4. header ที่ส่งไปมีเพียง content-type: application/json และ x-line-signature (หากมีในต้นทาง) โดย ไม่ได้ forward header อื่นเลย ไม่มีทั้ง user-agent และ x-forwarded-for
  5. เซ็ต req.Host ให้เป็น host ของ URL ปลายทาง
  6. response ถูก drain ทิ้งด้วย io.Copy(io.Discard) แล้วปิด connection โดย ไม่สนใจ status code หากปลายทางตอบ 500 ก็จะไม่มี log ไม่มี retry และไม่มี dead letter queue
  7. error ระดับ network จะ log warn เพียงอย่างเดียว

:::warning ข้อจำกัดที่ควรทราบ กลไกนี้ไม่มี retry ไม่มี circuit breaker และไม่เก็บสถิติใด ๆ หากระบบปลายทางของลูกค้าล่ม event ในช่วงเวลานั้นจะหายไปเงียบ ๆ และตรวจสอบย้อนหลังได้จาก log warn เท่านั้น :::

ไฟล์และฟังก์ชันหลัก

ไฟล์ฟังก์ชัน
internal/line/service.goService.forwardWebhook(rawURL, headers, body, raw) เป็นตัวหลัก
internal/line/service.goService.ProcessLine เป็นจุดเรียกทั้ง 2 เส้นทาง ผ่านค่า forwardWebhookUrl และ mCfg.LineWebhookURL
internal/line/service.gointerface httpDoer ซึ่ง *http.Client implement อยู่แล้ว ทำให้ mock ได้ใน test

HTTP client ที่ใช้เป็น &http.Client{} เปล่า ๆ ที่สร้างขึ้นใน line.Register โดยไม่ได้ใช้ internal/httpclient ของ template ที่มี retry และ backfoff ในตัว การควบคุมเวลาจึงใช้ context timeout แทน

จุดเชื่อมต่อกับ Service อื่น

  • Redis — ค่า forwardWebhookUrl และ mboxLineWebhookUrl มาจาก hash webhook_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