DEV Community

Cover image for Webhooks หรือ Polling: รูปแบบการเชื่อมต่อ API แบบไหนดีกว่ากัน
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

Webhooks หรือ Polling: รูปแบบการเชื่อมต่อ API แบบไหนดีกว่ากัน

สรุปสั้นๆ: Polling คือการตรวจสอบการอัปเดตเป็นระยะ (เรียบง่ายแต่ไม่มีประสิทธิภาพ) Webhooks คือการส่งการอัปเดตแบบเรียลไทม์ (มีประสิทธิภาพแต่ซับซ้อน) ใช้ Polling สำหรับการตรวจสอบที่ไม่บ่อยนัก และ Webhooks สำหรับการอัปเดตแบบเรียลไทม์ Modern PetstoreAPI รองรับทั้งสองรูปแบบพร้อมการส่ง Webhook ที่เชื่อถือได้

ทดลองใช้ Apidog วันนี้

ทำความเข้าใจความแตกต่าง

Polling: Client ถาม "มีการอัปเดตหรือไม่?" ซ้ำๆ Webhooks: Server บอกว่า "มีการอัปเดต!" เมื่อมีบางสิ่งเกิดขึ้น

คำอุปมา:

  • Polling = ตรวจสอบตู้จดหมายของคุณทุกชั่วโมง
  • Webhooks = บุรุษไปรษณีย์กดกริ่งเมื่อมีจดหมายมาส่ง

Polling: ทำงานอย่างไร

Client ทำการร้องขอเป็นระยะเพื่อตรวจสอบการเปลี่ยนแปลง

// Poll ทุก 30 วินาที
setInterval(async () => {
  const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
  const order = await response.json();

  if (order.status === 'completed') {
    console.log('Order completed!', order);
    clearInterval(pollInterval);
  }
}, 30000);

รูปแบบการ Polling:

Simple polling (การ Polling แบบง่าย):

GET /api/v1/orders/123
# Returns current order state (ส่งคืนสถานะคำสั่งซื้อปัจจุบัน)

Conditional polling (การ Polling แบบมีเงื่อนไข (ETag)):

GET /api/v1/orders/123
If-None-Match: "abc123"

# Returns 304 Not Modified if unchanged (ส่งคืน 304 Not Modified หากไม่มีการเปลี่ยนแปลง)
# Returns 200 with new data if changed (ส่งคืน 200 พร้อมข้อมูลใหม่หากมีการเปลี่ยนแปลง)

Since-based polling (การ Polling ตามเวลาที่กำหนด):

GET /api/v1/orders/123/events?since=1710331200
# Returns events since timestamp (ส่งคืนเหตุการณ์ตั้งแต่เวลาที่กำหนด)

Webhooks: ทำงานอย่างไร

Server ส่ง HTTP POST ไปยัง Endpoint ของคุณเมื่อมีเหตุการณ์เกิดขึ้น

ขั้นตอนการตั้งค่า:

// 1. Register webhook endpoint (ลงทะเบียน webhook endpoint)
POST /api/v1/webhooks
{
  "url": "https://myapp.com/webhooks/petstore",
  "events": ["order.created", "order.completed"],
  "secret": "whsec_abc123"
}

// 2. Server sends webhook when event occurs (Server ส่ง webhook เมื่อมีเหตุการณ์เกิดขึ้น)
POST https://myapp.com/webhooks/petstore
{
  "id": "evt_123",
  "type": "order.completed",
  "created": 1710331200,
  "data": {
    "orderId": "123",
    "status": "completed",
    "completedAt": "2024-01-01T12:00:00Z"
  }
}

// 3. Verify and process webhook (ตรวจสอบและประมวลผล webhook)
// Respond with 200 OK (ตอบกลับด้วย 200 OK)

ควรใช้ Polling เมื่อใด

เหมาะสำหรับ:

  • การตรวจสอบที่ไม่บ่อยนัก (ชั่วโมงละครั้ง)
  • ทรัพยากรจำนวนน้อย
  • การใช้งานที่เรียบง่าย
  • เมื่อคุณเป็นผู้ควบคุม Client
  • การทดสอบและดีบัก

ตัวอย่าง:

  • การตรวจสอบสถานะรายงานประจำวัน
  • การซิงค์ผู้ติดต่อทุกสองสามนาที
  • การตรวจสอบความสมบูรณ์ของเซิร์ฟเวอร์
  • การตรวจสอบสถานะการชำระเงิน (ไม่บ่อยนัก)

Polling ใช้ได้ดีเมื่อ:

  • การอัปเดตไม่บ่อยนัก
  • ความล่าช้าเล็กน้อยยอมรับได้
  • คุณต้องการการใช้งานที่เรียบง่าย
  • ทรัพยากรมีขนาดเล็ก

ควรใช้ Webhooks เมื่อใด

เหมาะสำหรับ:

  • การอัปเดตแบบเรียลไทม์
  • มีทรัพยากรจำนวนมากที่ต้องตรวจสอบ
  • เหตุการณ์ที่ต้องทันเวลา
  • การผสานรวมกับบุคคลที่สาม
  • การอัปเดตความถี่สูง

ตัวอย่าง:

  • การยืนยันการชำระเงิน
  • ข้อความแชท
  • การแจ้งเตือนราคาหุ้น
  • การเปลี่ยนแปลงสถานะคำสั่งซื้อ
  • การแจ้งเตือน CI/CD

Webhooks ดีกว่าเมื่อ:

  • การอัปเดตต้องเกิดขึ้นทันที
  • Polling ไม่มีประสิทธิภาพ
  • มี Client จำนวนมากตรวจสอบทรัพยากรเดียวกัน
  • คุณต้องการลดโหลดของเซิร์ฟเวอร์

ตารางเปรียบเทียบ

<!--kg-card-begin: html-->



















































ปัจจัย Polling Webhooks
ความหน่วง (Latency) ขึ้นอยู่กับช่วงเวลา Polling เรียลไทม์
โหลดเซิร์ฟเวอร์ สูง (คำขอเปล่าจำนวนมาก) ต่ำ (เฉพาะเหตุการณ์จริง)
ความซับซ้อน ง่าย ซับซ้อน
ความน่าเชื่อถือ สูง (Client ควบคุมการลองใหม่) ปานกลาง (ต้องมี Logic การลองใหม่)
การตั้งค่า ไม่มี การลงทะเบียน Endpoint
ปัญหาไฟร์วอลล์ ไม่มี (ขาออกเท่านั้น) อาจต้อง Whitelisting
ค่าใช้จ่าย สูงกว่า (คำขอมากกว่า) ต่ำกว่า (คำขอน้อยกว่า)
ดีที่สุดสำหรับ การตรวจสอบที่ไม่บ่อยนัก การอัปเดตแบบเรียลไทม์
<!--kg-card-end: html-->

การนำ Polling ไปใช้งาน

Basic Polling (การ Polling ขั้นพื้นฐาน)

async function pollOrderStatus(orderId, callback) {
  let lastStatus = null;

  const poll = async () => {
    try {
      const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
      const order = await response.json();

      // Only callback if status changed (เรียก Callback เฉพาะเมื่อสถานะเปลี่ยน)
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

      // Stop polling if terminal state (หยุด Polling หากถึงสถานะสุดท้าย)
      if (['completed', 'cancelled'].includes(order.status)) {
        return;
      }

      // Continue polling (ดำเนินการ Polling ต่อไป)
      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Polling error:', error);
      setTimeout(poll, 30000); // Back off on error (รอมากขึ้นเมื่อเกิดข้อผิดพลาด)
    }
  };

  poll();
}

// Usage (การใช้งาน)
pollOrderStatus('order-123', (order) => {
  console.log(`Order status: ${order.status}`);
});

Smart Polling (Exponential Backoff) (การ Polling อัจฉริยะ (การถอยกลับแบบทวีคูณ))

async function smartPoll(url, callback, options = {}) {
  const {
    maxRetries = 10,
    initialInterval = 1000,
    maxInterval = 60000,
    stopCondition = () => false
  } = options;

  let retries = 0;
  let interval = initialInterval;
  let lastData = null;

  const poll = async () => {
    try {
      const response = await fetch(url);
      const data = await response.json();

      // Callback if data changed (เรียก Callback หากข้อมูลเปลี่ยน)
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Stop if condition met (หยุดหากตรงตามเงื่อนไข)
      if (stopCondition(data)) {
        return;
      }

      // Reset interval on successful request (รีเซ็ตช่วงเวลาเมื่อคำขอสำเร็จ)
      interval = initialInterval;

    } catch (error) {
      retries++;
      if (retries >= maxRetries) {
        throw new Error('Max retries exceeded');
      }
    }

    // Schedule next poll with exponential backoff (กำหนดเวลา Polling ครั้งถัดไปด้วยการถอยกลับแบบทวีคูณ)
    setTimeout(poll, interval);
    interval = Math.min(interval * 2, maxInterval);
  };

  poll();
}

// Usage: Poll order until completed (การใช้งาน: Polling คำสั่งซื้อจนกว่าจะเสร็จสมบูรณ์)
smartPoll('https://petstoreapi.com/api/v1/orders/123',
  (order) => console.log('Order:', order),
  {
    stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
    initialInterval: 2000,
    maxInterval: 30000
  }
);

Polling with ETag (การ Polling ด้วย ETag)

async function pollWithEtag(url, callback) {
  let etag = null;

  const poll = async () => {
    const headers = {};
    if (etag) {
      headers['If-None-Match'] = etag;
    }

    const response = await fetch(url, { headers });

    if (response.status === 304) {
      // Not modified, continue polling (ไม่มีการแก้ไข ดำเนินการ Polling ต่อไป)
      setTimeout(poll, 30000);
      return;
    }

    const data = await response.json();
    etag = response.headers.get('etag');

    callback(data);
    setTimeout(poll, 30000);
  };

  poll();
}

การนำ Webhooks ไปใช้งาน

Registering Webhooks (การลงทะเบียน Webhooks)

// Register webhook endpoint (ลงทะเบียน webhook endpoint)
async function registerWebhook(url, events) {
  const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      url,
      events,
      secret: generateSecret()
    })
  });

  return response.json();
}

function generateSecret() {
  return 'whsec_' + crypto.randomBytes(32).toString('hex');
}

Receiving Webhooks (การรับ Webhooks)

const express = require('express');
const crypto = require('crypto');
const app = express();

// Raw body parser for signature verification (ตัวแยกวิเคราะห์เนื้อหาดิบสำหรับการตรวจสอบลายเซ็น)
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks/petstore', async (req, res) => {
  const signature = req.headers['x-petstore-signature'];
  const body = req.body;

  // Verify signature (ตรวจสอบลายเซ็น)
  const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(body.toString());

  // Process event (ประมวลผลเหตุการณ์)
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    case 'order.completed':
      await handleOrderCompleted(event.data);
      break;
    case 'order.cancelled':
      await handleOrderCancelled(event.data);
      break;
  }

  // Acknowledge receipt (ยืนยันการรับ)
  res.status(200).json({ received: true });
});

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

Testing Webhooks Locally (การทดสอบ Webhooks ในเครื่อง)

# Use ngrok to expose local endpoint (ใช้ ngrok เพื่อเปิดเผย endpoint ในเครื่อง)
ngrok http 3000

# Register ngrok URL as webhook endpoint (ลงทะเบียน URL ของ ngrok เป็น webhook endpoint)
curl -X POST https://petstoreapi.com/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/petstore",
    "events": ["order.created", "order.completed"]
  }'

Reliable Webhook Delivery (การส่ง Webhook ที่เชื่อถือได้)

Webhooks อาจล้มเหลวได้ ควรใช้ Logic การลองใหม่

Sender Side (Server) (ฝั่งผู้ส่ง (Server))

// Queue webhooks for delivery (จัดคิว webhooks สำหรับการจัดส่ง)
const webhookQueue = [];

async function sendWebhook(event) {
  const webhooks = await db.webhooks.findMany({
    where: { events: { contains: event.type } }
  });

  for (const webhook of webhooks) {
    webhookQueue.push({
      webhook,
      event,
      attempts: 0,
      nextAttempt: Date.now()
    });
  }

  processQueue();
}

async function processQueue() {
  const now = Date.now();

  for (const item of webhookQueue) {
    if (item.nextAttempt > now) continue;

    try {
      await deliverWebhook(item);
      // Remove from queue on success (ลบออกจากคิวเมื่อสำเร็จ)
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      // Schedule retry with exponential backoff (กำหนดเวลาลองใหม่ด้วยการถอยกลับแบบทวีคูณ)
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Mark as failed after 5 attempts (ทำเครื่องหมายว่าล้มเหลวหลังจาก 5 ครั้ง)
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1min, 5min, 15min, 1hr, 4hr
  const delays = [60000, 300000, 900000, 3600000, 14400000];
  return delays[attempt - 1] || delays[delays.length - 1];
}

async function deliverWebhook({ webhook, event }) {
  const signature = generateSignature(event, webhook.secret);

  const response = await fetch(webhook.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Petstore-Signature': signature,
      'X-Petstore-Event': event.type
    },
    body: JSON.stringify(event),
    timeout: 10000
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
}

Receiver Side (Client) (ฝั่งผู้รับ (Client))

// Idempotent webhook handling (การจัดการ Webhook แบบ Idempotent)
const processedEvents = new Set();

app.post('/webhooks/petstore', async (req, res) => {
  const event = JSON.parse(req.body.toString());

  // Skip if already processed (idempotency) (ข้ามหากประมวลผลไปแล้ว (idempotency))
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

  try {
    await processEvent(event);
    processedEvents.add(event.id);

    // Clean up old event IDs (keep last 1000) (ล้าง ID เหตุการณ์เก่า (เก็บ 1000 รายการล่าสุด))
    if (processedEvents.size > 1000) {
      const arr = Array.from(processedEvents);
      arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Return 5xx to trigger retry (ส่งคืน 5xx เพื่อเรียกการลองใหม่)
    res.status(500).json({ error: 'Processing failed' });
  }
});

async function processEvent(event) {
  // Process the event (ประมวลผลเหตุการณ์)
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    // ... handle other events (...จัดการเหตุการณ์อื่นๆ)
  }
}

Hybrid Approach (แนวทางแบบผสมผสาน)

ใช้ทั้ง Polling และ Webhooks สำหรับการอัปเดตที่สำคัญ

class OrderMonitor {
  constructor(orderId, callback) {
    this.orderId = orderId;
    this.callback = callback;
    this.pollInterval = null;
  }

  async start() {
    // Start with polling for immediate feedback (เริ่มต้นด้วย Polling เพื่อรับการตอบรับทันที)
    this.startPolling();

    // Register webhook for real-time update (ลงทะเบียน webhook สำหรับการอัปเดตแบบเรียลไทม์)
    await this.registerWebhook();
  }

  startPolling() {
    this.pollInterval = setInterval(async () => {
      const order = await this.fetchOrder();
      this.callback(order);

      if (['completed', 'cancelled'].includes(order.status)) {
        this.stop();
      }
    }, 10000);
  }

  async registerWebhook() {
    const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${TOKEN}` },
      body: JSON.stringify({
        url: 'https://myapp.com/webhooks/petstore',
        events: [`order.${this.orderId}`],
        oneTime: true // Auto-delete after first delivery (ลบอัตโนมัติหลังจากการส่งครั้งแรก)
      })
    });

    this.webhookId = (await response.json()).id;
  }

  stop() {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
    }
    if (this.webhookId) {
      fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
        method: 'DELETE'
      });
    }
  }
}

คำถามที่พบบ่อย

ถาม: ฉันควร Polling บ่อยแค่ไหน?ขึ้นอยู่กับความเร่งด่วน 30 วินาทีสำหรับการอัปเดตเกือบเรียลไทม์ 5 นาทีสำหรับเรื่องที่ไม่เร่งด่วน ควรสร้างสมดุลระหว่างความสดใหม่ของข้อมูลกับโหลดของเซิร์ฟเวอร์

ถาม: จะเกิดอะไรขึ้นถ้า webhook endpoint ของฉันล่ม?ผู้ให้บริการ Webhook ที่ดีจะลองใหม่ด้วย Exponential backoff ควรใช้ Idempotency เพื่อจัดการกับการส่งซ้ำ

ถาม: ฉันจะรักษาความปลอดภัยของ Webhooks ได้อย่างไร?ตรวจสอบลายเซ็นโดยใช้ Shared secrets ใช้อะไรรองรับ HTTPS เท่านั้น และตรวจสอบความถูกต้องของข้อมูลเหตุการณ์

ถาม: ฉันสามารถใช้ Webhooks สำหรับข้อมูลในอดีตได้หรือไม่?ไม่ได้ Webhooks มีไว้สำหรับเหตุการณ์ใหม่เท่านั้น ใช้ Polling หรือ Batch API สำหรับข้อมูลในอดีต

ถาม: ฉันควรใช้ Polling หรือ Webhooks สำหรับแอปพลิเคชันมือถือ?Polling นั้นง่ายกว่าสำหรับมือถือ Webhooks ต้องใช้ Push Notifications เป็นตัวกลาง

ถาม: ฉันจะดีบักปัญหา Webhook ได้อย่างไร?ใช้เครื่องมือเช่น webhook.site สำหรับการทดสอบ บันทึกการส่ง Webhook ทั้งหมด และจัดเตรียมประวัติเหตุการณ์ Webhook ใน API ของคุณ

Modern PetstoreAPI รองรับทั้ง Polling และ Webhooks ดู คู่มือ Webhooks สำหรับรายละเอียดการนำไปใช้งาน ทดสอบการผสานรวม Webhook ด้วย Apidog

Top comments (0)