ทำไม LINE Webhook ที่ตรวจ Signature ถูกแล้วยังโดน Cloudflare Workers ปฏิเสธ

สรุปสั้น ๆ
ทำ LINE Webhook ด้วย Cloudflare Workers ต้องตรวจ signature ด้วย HMAC-SHA256 ตาม format เฉพาะของ LINE ซึ่งใช้ channel secret เข้ารหัสแล้วเทียบแบบ base64 ไม่ใช่ hex เหมือน webhook ทั่วไป จุดที่ต่างจาก webhook อื่นชัดเจนคือ reply token มีอายุสั้นมากใช้ตอบกลับได้ครั้งเดียวในเวลาจำกัด และ payload หนึ่งครั้งอาจมีหลาย event ปนกันที่ต้องแยกประมวลผลทีละตัว ไม่ใช่ event เดียวต่อ request แบบ webhook ทั่วไป
การทำ webhook รับ event จาก LINE Messaging API มีโครงสร้างเฉพาะที่ต่างจาก webhook ทั่วไปพอสมควร ทีมที่เคยเขียน webhook receiver ให้บริการอื่นมาก่อนแล้วมาเขียนให้ LINE ครั้งแรก มักเจอปัญหาที่ signature ตรวจผ่านทุกอย่างตามที่เข้าใจ แต่ LINE Platform ยังปฏิเสธหรือ endpoint ทำงานไม่ถูกต้องตามที่คาด
สาเหตุหลักมาจากรายละเอียดเฉพาะของ LINE ที่ต่างจาก webhook ทั่วไป ทั้งรูปแบบการคำนวณ signature ที่ใช้ base64 แทน hex ที่คุ้นเคย โครงสร้าง payload ที่รวมหลาย event มาในการเรียกครั้งเดียว และกลไก reply token ที่มีอายุสั้นมากจนถ้าประมวลผลช้าไปนิดเดียวจะใช้ตอบกลับไม่ได้อีก
บทความนี้จะเจาะรายละเอียดเฉพาะของ LINE Webhook เมื่อสร้างบน Cloudflare Workers ตั้งแต่การตรวจ signature ให้ถูก format ไปจนถึงการจัดการหลาย event ในครั้งเดียวและเพดานเวลาของ reply token ที่ต้องออกแบบให้ทัน
รูปแบบ Signature ของ LINE ต่างจาก Webhook ทั่วไปตรงไหน
LINE Messaging API แนบ signature มากับ header ชื่อเฉพาะ คำนวณจาก request body ทั้งหมดด้วย HMAC-SHA256 โดยใช้ channel secret เป็น key เหมือนหลักการทั่วไปของ webhook signature แต่จุดที่ต่างคือผลลัพธ์ที่ได้ต้องเข้ารหัสเป็น base64 ก่อนเทียบกับค่าใน header ไม่ใช่แปลงเป็น hex string แบบที่ webhook หลายเจ้าใช้กัน
ทีมที่คุ้นเคยกับการตรวจ signature แบบ hex จาก provider อื่นมาก่อน แล้วนำโค้ดเดิมมาปรับใช้กับ LINE โดยลืมเปลี่ยนขั้นตอนสุดท้ายจาก hex เป็น base64 จะเจออาการ signature ไม่ตรงกันทุกครั้งทั้งที่ channel secret และ raw body ถูกต้องทุกอย่าง เพราะรูปแบบการเข้ารหัสผลลัพธ์สุดท้ายไม่ตรงกัน
เช่นเดียวกับ webhook ทั่วไป ต้องอ่าน raw body เป็น text ดิบมาคำนวณ signature ก่อน ไม่ parse เป็น JSON ก่อนคำนวณ Web Crypto API ที่ Cloudflare Workers รองรับอยู่แล้วสามารถคำนวณ HMAC-SHA256 แล้วแปลงผลลัพธ์เป็น base64 ได้โดยไม่ต้องพึ่ง library ภายนอก เพียงแต่ต้องเขียนขั้นตอนแปลงผลลัพธ์ให้ตรงกับที่ LINE คาดหวังเท่านั้น
หนึ่ง Payload มีหลาย Event ปนกัน ต้องแยกประมวลผลทีละตัว
webhook ทั่วไปหลายเจ้ามักส่งหนึ่ง event ต่อหนึ่ง request แต่ LINE Messaging API รวมหลาย event ที่เกิดขึ้นใกล้เคียงกันมาไว้ใน array เดียวกันภายใน payload เดียว เช่นถ้ามีผู้ใช้หลายคนส่งข้อความมาในเวลาใกล้เคียงกัน ทั้งหมดอาจถูกส่งมาใน request เดียวกันเป็น array ของ event หลายตัว
ผลที่ตามมาคือ Worker ต้อง loop ผ่านทุก event ใน array แล้วแยกประมวลผลทีละตัวตามประเภทของมัน เช่น event ประเภทข้อความ event ประเภทติดตาม หรือ event ประเภทเลิกติดตาม แต่ละประเภทมีโครงสร้างข้อมูลย่อยต่างกัน ถ้าโค้ดเขียนโดยคาดว่าจะมี event เดียวเสมอแล้วอ่านแค่ตัวแรกใน array จะทำให้ event ที่เหลือถูกมองข้ามไปเงียบ ๆ โดยไม่มี error ให้เห็น
แนวทางที่ปลอดภัยคือ loop ผ่านทุก event ก่อนตอบ response กลับให้ LINE Platform เพียงครั้งเดียว ไม่ใช่ตอบ response แยกทีละ event เพราะ LINE คาดหวัง response เดียวสำหรับทั้ง request ถ้า event บางตัวใน array ประมวลผลล้มเหลว ควรมีระบบ log แยกไว้ตรวจสอบทีหลัง แทนที่จะทำให้ทั้ง request ล้มเหลวเพราะ event เดียวมีปัญหา
Reply Token มีอายุสั้นมาก ออกแบบให้ทันเวลาตั้งแต่ต้น
แต่ละ event ที่มาพร้อมความสามารถตอบกลับผู้ใช้ได้ จะแนบ reply token มาด้วย ซึ่งใช้ได้ครั้งเดียวและมีอายุสั้นมาก ถ้า Worker ใช้เวลาประมวลผลนานเกินไปก่อนเรียก API ตอบกลับด้วย reply token นั้น token จะหมดอายุและตอบกลับไม่ได้อีก ต่างจาก webhook ทั่วไปที่มักไม่มีเงื่อนไขเรื่องเวลาผูกกับการตอบกลับผู้ใช้ปลายทางโดยตรงแบบนี้
ข้อจำกัดนี้ทำให้การออกแบบ Worker รับ LINE webhook ต้องต่างจาก webhook ทั่วไปที่เน้นแค่ตอบ HTTP response ให้เร็ว เพราะที่นี่ต้องเรียก API ตอบกลับผู้ใช้ให้ทันภายในเวลาจำกัดของ reply token ด้วย ถ้า logic การตัดสินใจว่าจะตอบอะไรกลับซับซ้อนหรือต้องรอข้อมูลจากภายนอกก่อน ควรพิจารณาตอบข้อความเบื้องต้นแบบง่ายก่อนด้วย reply token แล้วค่อยส่งข้อความเพิ่มเติมทีหลังผ่านช่องทางที่ไม่ผูกกับ reply token แทน
อีกทางเลือกคือใช้ API สำหรับส่งข้อความแบบที่ไม่ผูกกับ reply token ซึ่งไม่มีเงื่อนไขเรื่องเวลาจำกัดแบบเดียวกัน เหมาะกับกรณีที่ logic ตอบกลับต้องใช้เวลาประมวลผลนานกว่าที่ reply token จะรอไหว แต่ต้องเข้าใจว่า API แบบนี้มีเงื่อนไขการใช้งานและโควตาที่ต่างจากการตอบผ่าน reply token เช่นกัน ควรอ่านเงื่อนไขให้ครบก่อนออกแบบ flow ที่พึ่งพา API ประเภทนี้เป็นหลัก
LINE Platform ก็ Retry เหมือน Webhook ทั่วไป ไม่ใช่ข้อยกเว้น
แม้ LINE Webhook จะมีรายละเอียดเฉพาะหลายจุด แต่หลักการเรื่อง retry ยังเป็นไปตามหลักการทั่วไปของ webhook ที่อธิบายไว้ในบทความ สร้าง Webhook Receiver ด้วย Cloudflare Workers คือถ้า LINE Platform ไม่ได้รับ response ที่ถือว่าสำเร็จภายในเวลาที่กำหนด จะส่ง payload เดิมซ้ำเข้ามาอีก ทำให้ต้องมีกลไกกันประมวลผลซ้ำเช่นเดียวกับ webhook ทั่วไป
จุดที่ต้องระวังเพิ่มเติมคือ retry ของ LINE จะส่ง payload ทั้งชุดที่มีหลาย event ซ้ำเข้ามาทั้งหมด ไม่ใช่แค่ event ที่ยังไม่สำเร็จ ทำให้การตรวจ idempotency ต้องทำแยกเป็นระดับ event แต่ละตัวภายใน payload ไม่ใช่แค่ระดับ request ทั้งก้อน เพราะ payload เดิมที่ retry เข้ามาอาจมี event บางตัวที่เคยประมวลผลสำเร็จแล้วปนอยู่กับ event อื่นที่ยังไม่เคยเห็น หากมีการเพิ่ม event ใหม่มาในช่วงเวลาใกล้เคียงกัน
การเก็บ identifier ของแต่ละ event ไว้ตรวจสอบ ควรใช้ identifier ที่ LINE แนบมาให้ในแต่ละ event เอง ไม่ใช่สร้าง identifier ขึ้นเองจากเนื้อหา payload เพราะข้อความเดียวกันจากผู้ใช้คนเดียวกันอาจเกิดขึ้นซ้ำได้ในความเป็นจริง แต่ identifier ที่ LINE แนบมาให้จะไม่ซ้ำกันระหว่าง event ที่ต่างกันจริง
ตารางเทียบ LINE Webhook กับ Webhook ทั่วไปบน Cloudflare Workers
สรุปความต่างที่สำคัญที่สุดสำหรับทีมที่เคยเขียน webhook ทั่วไปมาก่อนแล้วมาเขียนให้ LINE ครั้งแรก
| ประเด็น | Webhook ทั่วไป | LINE Webhook |
|---|---|---|
| รูปแบบเข้ารหัส Signature | มักเป็น hex string | ต้องเป็น base64 |
| จำนวน Event ต่อ Request | มักเป็น event เดียวต่อ request | อาจมีหลาย event ปนกันใน array เดียว |
| เงื่อนไขเรื่องเวลาตอบกลับผู้ใช้ | ไม่มีผูกกับการตอบผู้ใช้ปลายทางโดยตรง | reply token มีอายุสั้นมาก ต้องตอบให้ทัน |
| พฤติกรรม Retry | ขึ้นกับนโยบายของแต่ละผู้ให้บริการ | retry ทั้ง payload ซ้ำ ต้องตรวจ idempotency ระดับ event |
ตัวอย่างสมมติ: Signature ไม่ตรงเพราะลืมเปลี่ยนจาก Hex เป็น Base64
ลองดูตัวอย่างสมมติของทีมที่เคยเขียน webhook receiver ให้ provider อื่นมาก่อน ซึ่งตรวจ signature แบบแปลงผลลัพธ์ HMAC เป็น hex string แล้วนำโค้ดชุดเดิมมาปรับใช้กับ LINE webhook โดยเปลี่ยนแค่ channel secret แต่ลืมเปลี่ยนขั้นตอนการเข้ารหัสผลลัพธ์สุดท้ายให้เป็น base64 (ตัวอย่างสมมติเพื่อประกอบการอธิบาย)
ผลคือทุก request จาก LINE Platform ถูกปฏิเสธเพราะ signature ที่คำนวณได้ไม่ตรงกับ header ที่แนบมา ทั้งที่ channel secret ถูกต้อง และ raw body ก็ไม่ได้ถูกแปลงผิดรูปแบบแต่อย่างใด ทีมใช้เวลาหลายชั่วโมงตรวจสอบ channel secret ซ้ำแล้วซ้ำเล่าเพราะสงสัยว่าตั้งค่าผิด ก่อนจะพบว่าปัญหาจริงอยู่ที่ขั้นตอนสุดท้ายของการเข้ารหัสผลลัพธ์
หลังแก้ไขให้แปลงผลลัพธ์ HMAC เป็น base64 ตามที่ LINE คาดหวัง signature ก็ตรงกันทันทีโดยไม่ต้องแก้ส่วนอื่นเลย บทเรียนจากเคสสมมตินี้คือรายละเอียดเล็ก ๆ อย่างรูปแบบการเข้ารหัสผลลัพธ์สุดท้าย สามารถทำให้ทั้งระบบใช้งานไม่ได้เลยแม้ทุกอย่างอื่นจะถูกต้อง ควรตรวจ documentation ของแต่ละผู้ให้บริการอย่างละเอียดทุกครั้งแทนการนำโค้ดจาก provider อื่นมาใช้ซ้ำโดยไม่ตรวจสอบรายละเอียด
Worker ไม่มี State ในตัวเอง เก็บ Event ที่ประมวลผลแล้วไว้ที่ไหน
จุดที่มักถูกมองข้ามเมื่อพูดถึงการตรวจ idempotency ระดับ event คือคำถามที่ว่าแล้วจะเก็บรายการ event identifier ที่ประมวลผลไปแล้วไว้ที่ไหน เพราะ Cloudflare Workers เป็น runtime แบบ stateless ทุก request คือการรันโค้ดใหม่จากศูนย์ ไม่มีตัวแปรในหน่วยความจำที่คงอยู่ข้าม request ได้แบบ server ทั่วไปที่รันเป็น process ต่อเนื่อง ทีมที่คุ้นกับการเขียน webhook บน server แบบเดิมมักลืมจุดนี้แล้วพยายามเก็บ state ไว้ในตัวแปรระดับโมดูล ซึ่งใช้ไม่ได้จริงเพราะ Worker แต่ละ instance อาจถูกสร้างใหม่หรือทำงานคู่ขนานหลาย instance พร้อมกันได้ตลอดเวลา
ทางเลือกที่ใช้กันทั่วไปคือ Workers KV ซึ่งเป็น key-value store ที่ผูกกับ Worker โดยตรง เหมาะกับการเก็บ identifier ของ event ที่ประมวลผลแล้วพร้อมตั้งค่าหมดอายุอัตโนมัติ เพราะไม่จำเป็นต้องเก็บ identifier เก่าไว้ตลอดไป เก็บไว้แค่ช่วงเวลาที่ LINE Platform ยังมีโอกาส retry payload เดิมเข้ามาก็เพียงพอ ก่อนเขียนโค้ดตรวจ event ใหม่ ให้เช็คก่อนว่า identifier นั้นมีอยู่ใน KV แล้วหรือยัง ถ้ามีแล้วให้ข้ามการประมวลผลซ้ำแต่ยังต้องตอบ response สำเร็จกลับไปตามปกติ
ข้อจำกัดที่ต้องรู้ก่อนใช้ KV คือมันเป็น eventually consistent ไม่ใช่ strongly consistent การเขียนค่าใหม่จาก edge หนึ่งอาจยังไม่ปรากฏให้ edge อื่นเห็นทันที ถ้า LINE Platform ยิง retry เข้ามาเร็วมากในช่วงเวลาไม่กี่วินาทีและ request ทั้งสองไปตกที่ edge คนละจุดกัน มีโอกาสน้อยที่ทั้งสอง request จะยังไม่เห็น identifier ตัวเดียวกันพร้อมกันจนประมวลผลซ้ำได้อยู่ดี สำหรับกรณีที่ต้องการความแม่นยำสูงกว่านั้น เช่น event ที่ผูกกับการตัดเงินหรือ transaction สำคัญ ควรพิจารณาใช้ Durable Object แทน เพราะ Durable Object รับประกันว่ามีเพียง instance เดียวที่จัดการ state ชุดหนึ่งในเวลาเดียวกัน ทำให้ตรวจสอบและเขียน state แบบ atomic ได้จริง แลกกับความซับซ้อนและ latency ที่เพิ่มขึ้นจากการเขียนโค้ดในรูปแบบที่ต่างจาก KV ทั่วไป
ทางเลือกที่สามคือส่ง event ที่ผ่านการกันซ้ำเบื้องต้นแล้วต่อไปยัง database ฝั่ง Supabase ที่มี unique constraint บนคอลัมน์ identifier ของ event โดยตรง ให้ database เป็นตัวปฏิเสธการเขียนซ้ำแทนที่จะพึ่งการเช็คก่อนเขียนฝั่ง Worker เพียงอย่างเดียว วิธีนี้ปลอดภัยกว่าการเช็คก่อนเขียนแบบ KV เพราะ unique constraint ทำงานแบบ atomic ที่ระดับ database เสมอ แต่ต้องแลกกับ latency ที่เพิ่มขึ้นจากการเรียก database ทุกครั้งที่มี event เข้ามา ทีมจึงมักเลือกผสมทั้งสองแบบ คือใช้ KV เป็นชั้นกรองเบื้องต้นให้เร็ว แล้วใช้ unique constraint ของ database เป็นชั้นป้องกันสุดท้ายอีกชั้นหนึ่ง
ทำแบบนี้แล้วพัง เพราะอะไร
ข้อผิดพลาดที่พบบ่อยเมื่อทีมสร้าง LINE Webhook บน Cloudflare Workers ครั้งแรกโดยไม่เข้าใจรายละเอียดเฉพาะของ LINE
- แปลงผลลัพธ์ HMAC เป็น hex แทน base64 — ทำให้ signature ไม่ตรงกับ header ของ LINE ทุกครั้งแม้ channel secret จะถูกต้อง
- อ่านแค่ event แรกใน array แล้วมองข้าม event ที่เหลือ — ทำให้ข้อความหรือ event บางส่วนของผู้ใช้หายไปเงียบ ๆ โดยไม่มี error ให้เห็น
- ทำงานหนักก่อนเรียก reply API — ทำให้ reply token หมดอายุก่อนตอบกลับผู้ใช้ได้ทัน กลายเป็นข้อความที่ผู้ใช้ส่งมาไม่ได้รับการตอบกลับเลย
- ตรวจ idempotency แค่ระดับ request ทั้งก้อน — พลาด event ใหม่ที่ปนมากับ retry ของ payload เดิมที่มี event เก่าที่เคยประมวลผลแล้วอยู่ด้วย
สรุป
LINE Webhook มีรายละเอียดเฉพาะที่ต่างจาก webhook ทั่วไปหลายจุด ทั้งรูปแบบ signature ที่ต้องเป็น base64 โครงสร้าง payload ที่รวมหลาย event มาด้วยกัน และเพดานเวลาสั้นของ reply token ที่ผูกกับการตอบกลับผู้ใช้โดยตรง ทีมที่นำโค้ด webhook ทั่วไปมาปรับใช้ต้องตรวจรายละเอียดเหล่านี้ให้ครบ ไม่ใช่แค่เปลี่ยน secret แล้วคาดว่าจะทำงานได้เหมือนเดิม
การออกแบบให้ทันเวลาของ reply token และตรวจ idempotency ที่ระดับ event แต่ละตัว เป็นสองเรื่องที่สำคัญไม่แพ้การตรวจ signature ให้ถูก format เพราะทั้งสามจุดนี้ล้วนเป็นสาเหตุของปัญหาที่พบบ่อยที่สุดเมื่อทำ LINE Webhook บน Cloudflare Workers
- แปลงผลลัพธ์ HMAC เป็น base64 ไม่ใช่ hex เมื่อตรวจ signature ของ LINE
- loop ประมวลผลทุก event ใน array ก่อนตอบ response เดียวกลับให้ LINE Platform
- ตรวจ idempotency ที่ระดับ event แต่ละตัวด้วย identifier ที่ LINE แนบมาให้ ไม่ใช่ระดับ request ทั้งก้อน
คำถามที่พบบ่อย
ทำไม LINE ใช้ base64 แทน hex สำหรับ Signature
เป็นข้อกำหนดเฉพาะของ LINE Messaging API เอง ไม่ใช่มาตรฐานตายตัวที่ webhook ทุกเจ้าต้องใช้เหมือนกัน ต้องอ่าน documentation ของแต่ละผู้ให้บริการเพื่อยืนยันรูปแบบที่ถูกต้องก่อนเขียนโค้ดตรวจสอบเสมอ
หนึ่ง Request จาก LINE มีได้กี่ Event
ไม่ตายตัว ขึ้นกับว่ามี event เกิดขึ้นใกล้เคียงกันกี่ตัวในช่วงเวลานั้น อาจมีตั้งแต่หนึ่งตัวไปจนถึงหลายตัวปนกันใน array เดียว โค้ดต้องรองรับทั้งสองกรณีเสมอ
Reply Token หมดอายุแล้วส่งข้อความกลับผู้ใช้ได้อีกไหม
ไม่ได้ด้วย reply token เดิม ต้องใช้ API สำหรับส่งข้อความแบบที่ไม่ผูกกับ reply token แทน ซึ่งมีเงื่อนไขและโควตาการใช้งานที่ต่างออกไป
ต้องตอบ Response แยกทีละ Event ในหนึ่ง Payload ไหม
ไม่ต้อง ควร loop ประมวลผลทุก event ในใจก่อนแล้วตอบ response เดียวกลับให้ LINE Platform เพียงครั้งเดียวสำหรับทั้ง request ไม่ใช่แยกตอบทีละ event
LINE Retry Payload เดิมทั้งชุดหรือแค่ Event ที่ล้มเหลว
retry ทั้ง payload ชุดเดิมทั้งหมด ไม่ได้แยกเฉพาะ event ที่ล้มเหลว ทำให้ต้องตรวจ idempotency ที่ระดับ event แต่ละตัวโดยใช้ identifier ที่ LINE แนบมาให้ ไม่ใช่ระดับ request ทั้งก้อน
ควรใช้ identifier แบบไหนตรวจ Event ซ้ำจาก LINE
ควรใช้ identifier เฉพาะตัวที่ LINE แนบมาให้ในแต่ละ event โดยตรง ไม่ควรสร้าง identifier เองจากเนื้อหาข้อความ เพราะข้อความเดียวกันจากผู้ใช้คนเดียวกันอาจเกิดขึ้นซ้ำได้จริงโดยไม่ใช่การ retry
ลองตรวจด้วยตัวเอง
LINE Webhook Signature Validator
ทดสอบ signature กับ raw body และ channel secret ของคุณได้ในเบราว์เซอร์ — secret ไม่ถูกส่งขึ้นเซิร์ฟเวอร์
ตรวจ signature ฟรี →ใช้ฟรี ไม่ต้องสมัคร ไม่เก็บข้อมูลที่คุณกรอก
อ่านต่อแบบเจาะลึก
วัด Conversion LINE ทุกแบรนด์ รวมศูนย์ที่เดียว
องค์กรที่มีหลายแบรนด์ หลายสาขา หลาย LINE OA — linli รวม Tracking และส่ง Conversion กลับทุกแพลตฟอร์ม พร้อม Custom Limit เริ่มฟรี 14 วัน
ติดต่อทีม Salesบทความที่เกี่ยวข้อง

ทราฟฟิกพุ่งกลางดึกแต่ Cloudflare Workers เชื่อม Supabase Postgres ไว้ทัน แก้ยังไง

ตั้ง LINE Webhook บน Supabase Edge Functions ออกแบบตาราง Event ยังไงให้ไม่พังตอนข้อมูลเยอะ
