æ°åãæ°çŸäžã®ã¡ãŒã«ã¢ãã¬ã¹ãæ€èšŒããéãåçµæãåæçã«åŸ ã€ã®ã¯çŸå®çã§ã¯ãããŸãããã¡ãŒã«æ€èšŒWebhookã¯ãæ€èšŒã¿ã¹ã¯ãå®äºãããšãã«ã¢ããªã±ãŒã·ã§ã³ã«éç¥ããããšã§ããšã¬ã¬ã³ããªãœãªã¥ãŒã·ã§ã³ãæäŸãã宿çãªããŒãªã³ã°ã®å¿ èŠæ§ãæé€ããå¹ççãªéåæã¯ãŒã¯ãããŒãå¯èœã«ããŸãããã®å æ¬çãªã¬ã€ãã§ã¯ãåºæ¬çãªã»ããã¢ããããå€§èŠæš¡ãªæ€èšŒæäœãåŠçããããã®é«åºŠãªãã¿ãŒã³ãŸã§ãã¡ãŒã«æ€èšŒWebhookã®å®è£ ã«ã€ããŠéçºè ãç¥ãå¿ èŠããããã¹ãŠãæ¢ããŸãã
ã¡ãŒã«æ€èšŒWebhookã®çè§£
Webhookã¯HTTPã³ãŒã«ããã¯ã§ãããç¹å®ã®ã€ãã³ããçºçãããšãã«ã¢ããªã±ãŒã·ã§ã³ã«ããŒã¿ãé ä¿¡ããŸããã¡ãŒã«æ€èšŒã®ã³ã³ããã¹ãã§ã¯ãWebhookã¯äžæ¬æ€èšŒãžã§ããå®äºãããšããéåæã¢ãŒãã§åã ã®ã¡ãŒã«æ€èšŒãçµäºãããšãããŸãã¯æ€èšŒããã»ã¹äžã«ä»ã®éèŠãªã€ãã³ããçºçãããšãã«ã·ã¹ãã ã«éç¥ããŸãã
ã¡ãŒã«æ€èšŒã«Webhookã䜿çšããçç±
åŸæ¥ã®ãªã¯ãšã¹ãã»ã¬ã¹ãã³ã¹ãã¿ãŒã³ã¯åäžã®ã¡ãŒã«æ€èšŒã«ã¯é©ããŠããŸãããäžæ¬æäœã«ã¯èª²é¡ããããŸãã10äžéã®ã¡ãŒã«ãæ€èšŒããã«ã¯æ°æéãããå¯èœæ§ããããHTTPæ¥ç¶ãããã»ã©é·ãéãããŸãŸã«ããããšã¯çŸå®çã§ã¯ãããŸãããã¹ããŒã¿ã¹æŽæ°ã®ããŒãªã³ã°ã¯ãªãœãŒã¹ã浪費ããäžå¿ èŠãªAPIè² è·ãçã¿åºããŸãã
ããŒãªã³ã°ãªãŒããŒãããã®æé€
Webhookããªããã°ãäžæ¬ãžã§ããå®äºãããã©ããã確èªããããã«ãAPIã«ç¹°ãè¿ãã¯ãšãªãå®è¡ããå¿ èŠããããŸããããã«ãããäžå¿ èŠãªãããã¯ãŒã¯ãã©ãã£ãã¯ãçºçããAPIã¬ãŒãå¶éãæ¶è²»ãããã¢ããªã±ãŒã·ã§ã³ã«è€éããå ãããŸããWebhookã¯ãå¿ èŠãªãšãã«æ£ç¢ºã«éç¥ãããã·ã¥ããŸãã
ãªã¢ã«ã¿ã€ã åŠç
Webhookã¯ãæ€èšŒãå®äºãããšãã«å³åº§ã«ã¢ã¯ã·ã§ã³ãå®è¡ã§ããããã«ããŸããã¢ããªã±ãŒã·ã§ã³ã¯ãããŒãªã³ã°ééã«ãã£ãŠçããé å»¶ãªãã«ãçµæãåŠçããããŒã¿ããŒã¹ãæŽæ°ãããã©ããŒã¢ããã¢ã¯ã·ã§ã³ãããªã¬ãŒã§ããŸãã
ã¹ã±ãŒã©ãã«ãªã¢ãŒããã¯ãã£
WebhookããŒã¹ã®ã¢ãŒããã¯ãã£ã¯èªç¶ã«ã¹ã±ãŒã«ããŸãã1ã€ã®äžæ¬ãžã§ããåŠçããŠããå Žåã§ããåæã«æ°çŸãåŠçããŠããå Žåã§ããWebhookãšã³ããã€ã³ãã¯å°çããéç¥ãåä¿¡ãããã¥ãŒãã¯ãŒã«ãŒã䜿çšããŠéåæã«åŠçã§ããŸãã
ãªãœãŒã¹å¹ç
æ¥ç¶ãç¶æãããããŒãªã³ã°ã«ãŒããå®è¡ããããã代ããã«ãã¢ããªã±ãŒã·ã§ã³ã¯Webhookãå°çãããŸã§ã¢ã€ãã«ç¶æ ã®ãŸãŸã§ããããã«ãããèšç®ã³ã¹ããåæžãããã€ã³ãã©ã¹ãã©ã¯ãã£èŠä»¶ãç°¡çŽ åãããŸãã
ã¡ãŒã«æ€èšŒã«ãããWebhookã€ãã³ã
ã¡ãŒã«æ€èšŒãµãŒãã¹ã¯ãéåžžãããã€ãã®ã€ãã³ãã¿ã€ãã«å¯ŸããŠWebhookãããªã¬ãŒããŸãã
äžæ¬ãžã§ãå®äº
æãäžè¬çãªWebhookã€ãã³ãã¯ãäžæ¬æ€èšŒãžã§ãã®åŠçãå®äºãããšãã«çºç«ããŸãããã€ããŒãã«ã¯ããžã§ãã¹ããŒã¿ã¹ããµããªãŒçµ±èšãããã³çµæã®ããŠã³ããŒãã«é¢ããæ å ±ãå«ãŸããŸãã
äžæ¬ãžã§ã鲿
äžéšã®ãµãŒãã¹ã¯ãäžæ¬åŠçäžã«ééã眮ããŠé²æWebhookãéä¿¡ããæ€èšŒã®é²æã远跡ããå®äºæéãæšå®ã§ããããã«ããŸãã
äžæ¬ãžã§ã倱æ
äžæ¬ãžã§ããå®äºã劚ãããšã©ãŒã«ééããå Žåã倱æWebhookã¯äœãåé¡ã ã£ãããããã³éšåçãªçµæãå©çšå¯èœãã©ããã®è©³çްãæäŸããŸãã
åäžã¡ãŒã«æ€èšŒïŒéåæã¢ãŒãïŒ
倧éã®ãªã¢ã«ã¿ã€ã æ€èšŒã·ããªãªã®å Žåãéåæåäžã¡ãŒã«æ€èšŒã¯ãåæã¬ã¹ãã³ã¹ãåŸ ã€ä»£ããã«ãWebhookãä»ããŠçµæãéä¿¡ããŸãã
Webhookãšã³ããã€ã³ãã®ã»ããã¢ãã
Webhookãå®è£ ããã«ã¯ãWebhookãã€ããŒããåä¿¡ããŠåŠçã§ãããšã³ããã€ã³ããã¢ããªã±ãŒã·ã§ã³ã«äœæããå¿ èŠããããŸãã
åºæ¬çãªãšã³ããã€ã³ãæ§é
Webhookãšã³ããã€ã³ãã¯ãåã«JSONãã€ããŒããåãå ¥ããHTTP POSTãšã³ããã€ã³ãã§ãã
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/email-verification', async (req, res) => {
const { event_type, job_id, status, data } = req.body;
console.log(`Received webhook: ${event_type} for job ${job_id}`);
// Process the webhook
try {
await handleWebhookEvent(req.body);
// Always respond quickly to acknowledge receipt
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
// Still acknowledge receipt to prevent retries
res.status(200).json({ received: true, error: error.message });
}
});
async function handleWebhookEvent(payload) {
switch (payload.event_type) {
case 'bulk.completed':
await handleBulkCompleted(payload);
break;
case 'bulk.failed':
await handleBulkFailed(payload);
break;
case 'bulk.progress':
await handleBulkProgress(payload);
break;
default:
console.log(`Unknown event type: ${payload.event_type}`);
}
}
Webhookã¬ã¹ãã³ã¹ã®ãã¹ããã©ã¯ãã£ã¹
ã¡ãŒã«æ€èšŒãµãŒãã¹ã¯ãWebhookãšã³ããã€ã³ãããã®è¿ éãªã¬ã¹ãã³ã¹ãæåŸ ããŠããŸãããšã³ããã€ã³ããã¬ã¹ãã³ã¹ã«æéããããããããšããµãŒãã¹ã¯é ä¿¡ã倱æãããšèŠãªããå詊è¡ããå¯èœæ§ããããŸãã
å³åº§ã«ã¬ã¹ãã³ã¹
Webhookã®åä¿¡ãå³åº§ã«ç¢ºèªããŠããããã€ããŒããéåæã«åŠçããŸãã
app.post('/webhooks/email-verification', async (req, res) => {
// Immediately acknowledge receipt
res.status(200).json({ received: true });
// Process asynchronously
setImmediate(async () => {
try {
await handleWebhookEvent(req.body);
} catch (error) {
console.error('Async webhook processing error:', error);
// Log for retry or manual processing
await logFailedWebhook(req.body, error);
}
});
});
éãåŠçã«ã¯ã¡ãã»ãŒãžãã¥ãŒã䜿çš
æ¬çªã·ã¹ãã ã§ã¯ãWebhookãã€ããŒãããã¥ãŒã«å ¥ããŠã¯ãŒã«ãŒããã»ã¹ã§åŠçããŸãã
const Queue = require('bull');
const webhookQueue = new Queue('email-verification-webhooks');
app.post('/webhooks/email-verification', async (req, res) => {
// Queue the webhook for processing
await webhookQueue.add('process-webhook', req.body, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000
}
});
res.status(200).json({ received: true });
});
// Worker process
webhookQueue.process('process-webhook', async (job) => {
const payload = job.data;
await handleWebhookEvent(payload);
});
APIã§Webhookãèšå®
ã¡ãŒã«æ€èšŒãµãŒãã¹ã«Webhookãšã³ããã€ã³ããç»é²ããŸãã
async function registerWebhook(webhookUrl, events, secret) {
const response = await fetch('https://api.billionverify.com/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BILLIONVERIFY_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: webhookUrl,
events: events,
secret: secret
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(`Failed to register webhook: ${result.error}`);
}
console.log(`Webhook registered: ${result.webhook_id}`);
return result;
}
// Register for bulk job events
await registerWebhook(
'https://yourapp.com/webhooks/email-verification',
['bulk.completed', 'bulk.failed', 'bulk.progress'],
process.env.WEBHOOK_SECRET
);
Webhookãšã³ããã€ã³ãã®ã»ãã¥ãªãã£ä¿è·
Webhookãšã³ããã€ã³ãã¯å ¬éã¢ã¯ã»ã¹å¯èœã§ãããããã»ãã¥ãªãã£ãäžå¯æ¬ ã§ããé©åãªæ€èšŒããªããã°ãæ»æè ãã¢ããªã±ãŒã·ã§ã³ãæäœããããã«åœã®Webhookãã€ããŒããéä¿¡ããå¯èœæ§ããããŸãã
çœ²åæ€èšŒ
ã»ãšãã©ã®ã¡ãŒã«æ€èšŒãµãŒãã¹ã¯ãå ±æã·ãŒã¯ã¬ããã䜿çšããŠHMAC-SHA256ã§Webhookãã€ããŒãã«çœ²åããŸããåŠçããåã«çœ²åãæ€èšŒããŸãã
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhooks/email-verification', async (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
const isValid = verifyWebhookSignature(
req.body,
signature,
process.env.WEBHOOK_SECRET
);
if (!isValid) {
console.warn('Invalid webhook signature received');
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature valid, process webhook
await handleWebhookEvent(req.body);
res.status(200).json({ received: true });
});
ã¿ã€ã ã¹ã¿ã³ãæ€èšŒ
Webhookã®ã¿ã€ã ã¹ã¿ã³ããæ€èšŒããŠãªãã¬ã€æ»æãé²ããŸãã
function isTimestampValid(timestamp, toleranceSeconds = 300) {
const webhookTime = new Date(timestamp).getTime();
const currentTime = Date.now();
const difference = Math.abs(currentTime - webhookTime);
return difference <= toleranceSeconds * 1000;
}
app.post('/webhooks/email-verification', async (req, res) => {
const { timestamp } = req.body;
if (!isTimestampValid(timestamp)) {
console.warn('Webhook timestamp outside acceptable range');
return res.status(400).json({ error: 'Invalid timestamp' });
}
// Continue with signature verification and processing
});
IPèš±å¯ãªã¹ã
远å ã®ã»ãã¥ãªãã£ã®ããã«ãWebhookã¢ã¯ã»ã¹ãæ¢ç¥ã®IPã¢ãã¬ã¹ã«å¶éããŸãã
const allowedIPs = [
'203.0.113.0/24', // BillionVerify webhook servers
'198.51.100.0/24'
];
function isIPAllowed(clientIP) {
// Implement CIDR range checking
return allowedIPs.some(range => isIPInRange(clientIP, range));
}
app.post('/webhooks/email-verification', async (req, res) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (!isIPAllowed(clientIP)) {
console.warn(`Webhook from unauthorized IP: ${clientIP}`);
return res.status(403).json({ error: 'Forbidden' });
}
// Continue with processing
});
åªçæ§ã®åŠç
Webhookã¯ããããã¯ãŒã¯ã®åé¡ãå詊è¡ã«ãããè€æ°åé ä¿¡ãããå¯èœæ§ããããŸããéè€ãå®å šã«åŠçããããã«åªçæ§ãå®è£ ããŸãã
const processedWebhooks = new Set(); // Use Redis in production
async function handleWebhookIdempotent(payload) {
const webhookId = payload.webhook_id || payload.event_id;
// Check if already processed
if (processedWebhooks.has(webhookId)) {
console.log(`Duplicate webhook ignored: ${webhookId}`);
return;
}
// Mark as processing
processedWebhooks.add(webhookId);
try {
await handleWebhookEvent(payload);
} catch (error) {
// Remove from processed set to allow retry
processedWebhooks.delete(webhookId);
throw error;
}
}
æ¬çªã·ã¹ãã ã§ã¯ã忣åªçæ§ã«Redisã䜿çšããŸãã
const Redis = require('ioredis');
const redis = new Redis();
async function isWebhookProcessed(webhookId) {
const key = `webhook:processed:${webhookId}`;
const result = await redis.set(key, '1', 'NX', 'EX', 86400); // 24 hour expiry
return result === null; // Already exists
}
app.post('/webhooks/email-verification', async (req, res) => {
const webhookId = req.body.webhook_id;
if (await isWebhookProcessed(webhookId)) {
console.log(`Duplicate webhook: ${webhookId}`);
return res.status(200).json({ received: true, duplicate: true });
}
await handleWebhookEvent(req.body);
res.status(200).json({ received: true });
});
Webhookãã€ããŒãã®åŠç
ç°ãªãWebhookã€ãã³ãã«ã¯ç°ãªãåŠçããžãã¯ãå¿ èŠã§ããã¡ãŒã«æ€èšŒWebhookãåŠçããããã®äžè¬çãªãã¿ãŒã³ãæ¢ããŸãããã
äžæ¬ãžã§ãå®äºã®åŠç
äžæ¬æ€èšŒãžã§ããå®äºããããçµæãããŠã³ããŒãããŠåŠçããŸãã
async function handleBulkCompleted(payload) {
const { job_id, status, summary, download_url } = payload;
console.log(`Bulk job ${job_id} completed with status: ${status}`);
console.log(`Summary: ${summary.valid} valid, ${summary.invalid} invalid`);
// Download results
const results = await downloadResults(download_url);
// Process results
await processVerificationResults(job_id, results);
// Update job status in database
await updateJobStatus(job_id, 'completed', summary);
// Notify relevant parties
await sendCompletionNotification(job_id, summary);
}
async function downloadResults(url) {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${process.env.BILLIONVERIFY_API_KEY}`
}
});
if (!response.ok) {
throw new Error(`Failed to download results: ${response.status}`);
}
return await response.json();
}
async function processVerificationResults(jobId, results) {
// Batch update contacts in database
const validEmails = results.filter(r => r.is_valid);
const invalidEmails = results.filter(r => !r.is_valid);
await db.transaction(async (trx) => {
// Update valid emails
for (const batch of chunkArray(validEmails, 1000)) {
await trx('contacts')
.whereIn('email', batch.map(r => r.email))
.update({
email_verified: true,
verification_date: new Date(),
verification_job_id: jobId
});
}
// Handle invalid emails
for (const batch of chunkArray(invalidEmails, 1000)) {
await trx('contacts')
.whereIn('email', batch.map(r => r.email))
.update({
email_verified: false,
email_invalid_reason: trx.raw('CASE email ' +
batch.map(r => `WHEN '${r.email}' THEN '${r.reason}'`).join(' ') +
' END'),
verification_date: new Date(),
verification_job_id: jobId
});
}
});
}
äžæ¬ãžã§ã倱æã®åŠç
ãžã§ãã倱æããå Žåããšã©ãŒæ å ±ãååŸããå埩ãå¯èœãã©ããã倿ããŸãã
async function handleBulkFailed(payload) {
const { job_id, error_code, error_message, partial_results_available } = payload;
console.error(`Bulk job ${job_id} failed: ${error_message}`);
// Update job status
await updateJobStatus(job_id, 'failed', {
error_code,
error_message
});
// Try to retrieve partial results if available
if (partial_results_available) {
console.log('Attempting to retrieve partial results...');
try {
const partialResults = await downloadPartialResults(job_id);
await processVerificationResults(job_id, partialResults);
// Identify unprocessed emails for retry
const processedEmails = new Set(partialResults.map(r => r.email));
const originalEmails = await getOriginalJobEmails(job_id);
const unprocessedEmails = originalEmails.filter(e => !processedEmails.has(e));
if (unprocessedEmails.length > 0) {
// Schedule retry for unprocessed emails
await scheduleRetryJob(job_id, unprocessedEmails);
}
} catch (error) {
console.error('Failed to retrieve partial results:', error);
}
}
// Notify about failure
await sendFailureNotification(job_id, error_message);
}
async function scheduleRetryJob(originalJobId, emails) {
// Create new job for remaining emails
const response = await fetch('https://api.billionverify.com/v1/bulk/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BILLIONVERIFY_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
emails,
metadata: {
retry_of: originalJobId
}
})
});
const { job_id: newJobId } = await response.json();
console.log(`Scheduled retry job ${newJobId} for ${emails.length} emails`);
}
é²ææŽæ°ã®åŠç
鲿Webhookã¯ãé·æéå®è¡ããããžã§ãã®è¿œè·¡ã«åœ¹ç«ã¡ãŸãã
async function handleBulkProgress(payload) {
const { job_id, processed_count, total_count, estimated_completion } = payload;
const percentComplete = Math.round((processed_count / total_count) * 100);
console.log(`Job ${job_id}: ${percentComplete}% complete (${processed_count}/${total_count})`);
// Update progress in database
await updateJobProgress(job_id, {
processed_count,
total_count,
percent_complete: percentComplete,
estimated_completion: new Date(estimated_completion)
});
// Optionally notify users of progress
if (percentComplete % 25 === 0) {
await sendProgressNotification(job_id, percentComplete);
}
}
é«åºŠãªWebhookãã¿ãŒã³
æ¬çªã·ã¹ãã ã¯ãä¿¡é Œæ§ãšä¿å®æ§ãåäžãããé«åºŠãªãã¿ãŒã³ããæ©æµãåããŸãã
倱æããWebhookã®ãããã¬ã¿ãŒãã¥ãŒ
Webhookã®åŠçãç¹°ãè¿ã倱æããå Žåãæåã¬ãã¥ãŒã®ããã«ãã€ããŒãããããã¬ã¿ãŒãã¥ãŒã«ç§»åããŸãã
const webhookQueue = new Queue('email-verification-webhooks');
const deadLetterQueue = new Queue('webhook-dead-letters');
webhookQueue.process('process-webhook', async (job) => {
try {
await handleWebhookEvent(job.data);
} catch (error) {
// Check if this is the final retry
if (job.attemptsMade >= job.opts.attempts - 1) {
// Move to dead letter queue
await deadLetterQueue.add('failed-webhook', {
original_payload: job.data,
error: error.message,
failed_at: new Date().toISOString(),
attempts: job.attemptsMade + 1
});
}
throw error; // Re-throw to trigger retry
}
});
// Process dead letters manually or with alerts
deadLetterQueue.on('completed', async (job) => {
await sendAlert({
type: 'webhook_dead_letter',
job_id: job.data.original_payload.job_id,
error: job.data.error
});
});
Webhookã€ãã³ããœãŒã·ã³ã°
ç£æ»èšŒè·¡ãšãªãã¬ã€æ©èœã®ããã«ããã¹ãŠã®Webhookã€ãã³ããä¿åããŸãã
async function handleWebhookWithEventSourcing(payload) {
// Store raw event
const eventId = await storeWebhookEvent(payload);
try {
// Process event
await handleWebhookEvent(payload);
// Mark as processed
await markEventProcessed(eventId);
} catch (error) {
// Mark as failed
await markEventFailed(eventId, error);
throw error;
}
}
async function storeWebhookEvent(payload) {
const result = await db('webhook_events').insert({
event_type: payload.event_type,
job_id: payload.job_id,
payload: JSON.stringify(payload),
received_at: new Date(),
status: 'pending'
});
return result[0];
}
// Replay failed events
async function replayFailedEvents() {
const failedEvents = await db('webhook_events')
.where('status', 'failed')
.where('retry_count', '<', 3);
for (const event of failedEvents) {
try {
await handleWebhookEvent(JSON.parse(event.payload));
await markEventProcessed(event.id);
} catch (error) {
await incrementRetryCount(event.id);
}
}
}
ãã«ãããã³ãWebhookã«ãŒãã£ã³ã°
SaaSã¢ããªã±ãŒã·ã§ã³ã®å ŽåãWebhookãããã³ãåºæã®ãã³ãã©ã«ã«ãŒãã£ã³ã°ããŸãã
async function handleMultiTenantWebhook(payload) {
const { tenant_id, event_type, data } = payload;
// Get tenant configuration
const tenant = await getTenantConfig(tenant_id);
if (!tenant) {
console.error(`Unknown tenant: ${tenant_id}`);
return;
}
// Route to tenant-specific handler
switch (event_type) {
case 'bulk.completed':
await handleTenantBulkCompleted(tenant, data);
break;
case 'bulk.failed':
await handleTenantBulkFailed(tenant, data);
break;
}
// Forward to tenant webhook if configured
if (tenant.webhook_url) {
await forwardToTenant(tenant.webhook_url, tenant.webhook_secret, payload);
}
}
async function forwardToTenant(url, secret, payload) {
const signature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature
},
body: JSON.stringify(payload)
});
}
ãšã©ãŒåŠçãšä¿¡é Œæ§
å ç¢ãªWebhookå®è£ ã¯ãé害ãé©åã«åŠçããããŒã¿ã倱ãããªãããšãä¿èšŒããŸãã
åè©Šè¡æŠç¥
äžæçãªé害ã«å¯ŸããŠææ°ããã¯ãªããå®è£ ããŸãã
async function processWebhookWithRetry(payload, maxRetries = 5) {
const delays = [1000, 5000, 30000, 120000, 300000]; // 1s, 5s, 30s, 2m, 5m
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
await handleWebhookEvent(payload);
return; // Success
} catch (error) {
const isRetryable = isRetryableError(error);
if (!isRetryable || attempt === maxRetries - 1) {
// Log to dead letter queue
await logFailedWebhook(payload, error, attempt + 1);
throw error;
}
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delays[attempt]}ms`);
await sleep(delays[attempt]);
}
}
}
function isRetryableError(error) {
// Network errors, timeouts, and 5xx responses are retryable
const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
return retryableCodes.includes(error.code) ||
(error.status && error.status >= 500);
}
ãµãŒããããã¬ãŒã«ãŒãã¿ãŒã³
ããŠã³ã¹ããªãŒã ãµãŒãã¹ãå©çšã§ããªãå Žåã®ã«ã¹ã±ãŒãé害ãé²ããŸãã
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 60000;
this.state = 'CLOSED';
this.failures = 0;
this.lastFailure = null;
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailure > this.resetTimeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
console.warn('Circuit breaker opened due to failures');
}
}
}
const databaseCircuitBreaker = new CircuitBreaker();
async function handleBulkCompletedSafely(payload) {
await databaseCircuitBreaker.execute(async () => {
await processVerificationResults(payload.job_id, payload.results);
});
}
ã¢ãã¿ãªã³ã°ãšã¢ã©ãŒã
Webhookã®å¥å šæ§ã¡ããªã¯ã¹ã远跡ããŸãã
const metrics = {
received: 0,
processed: 0,
failed: 0,
latency: []
};
app.post('/webhooks/email-verification', async (req, res) => {
const startTime = Date.now();
metrics.received++;
try {
await handleWebhookEvent(req.body);
metrics.processed++;
} catch (error) {
metrics.failed++;
throw error;
} finally {
metrics.latency.push(Date.now() - startTime);
// Keep only last 1000 measurements
if (metrics.latency.length > 1000) {
metrics.latency.shift();
}
}
res.status(200).json({ received: true });
});
// Expose metrics endpoint
app.get('/metrics/webhooks', (req, res) => {
const avgLatency = metrics.latency.reduce((a, b) => a + b, 0) / metrics.latency.length;
res.json({
received: metrics.received,
processed: metrics.processed,
failed: metrics.failed,
success_rate: (metrics.processed / metrics.received * 100).toFixed(2) + '%',
avg_latency_ms: Math.round(avgLatency)
});
});
// Alert on high failure rate
setInterval(() => {
const failureRate = metrics.failed / metrics.received;
if (failureRate > 0.1) { // More than 10% failures
sendAlert({
type: 'high_webhook_failure_rate',
failure_rate: failureRate,
total_received: metrics.received,
total_failed: metrics.failed
});
}
}, 60000);
Webhookå®è£ ã®ãã¹ã
培åºçãªãã¹ãã«ãããWebhookãã³ãã©ãæ¬çªç°å¢ã§æ£ããåäœããããšãä¿èšŒããŸãã
ngrokã䜿çšããããŒã«ã«ãã¹ã
ngrokã䜿çšããŠãWebhookãã¹ãçšã«ããŒã«ã«ãšã³ããã€ã³ããå ¬éããŸãã
# Start your local server node server.js # In another terminal, expose it via ngrok ngrok http 3000
éçºäžã¯ãngrok URLãWebhookãšã³ããã€ã³ããšããŠç»é²ããŸãã
ã¢ãã¯Webhookãã€ããŒã
ç°ãªãã€ãã³ãã¿ã€ãã®ãã¹ããã£ã¯ã¹ãã£ãäœæããŸãã
const mockPayloads = {
bulkCompleted: {
event_type: 'bulk.completed',
job_id: 'job_123456',
status: 'completed',
timestamp: new Date().toISOString(),
summary: {
total: 1000,
valid: 850,
invalid: 120,
risky: 30
},
download_url: 'https://api.billionverify.com/v1/bulk/download/job_123456'
},
bulkFailed: {
event_type: 'bulk.failed',
job_id: 'job_789012',
error_code: 'PROCESSING_ERROR',
error_message: 'Internal processing error',
partial_results_available: true
},
bulkProgress: {
event_type: 'bulk.progress',
job_id: 'job_345678',
processed_count: 5000,
total_count: 10000,
estimated_completion: new Date(Date.now() + 3600000).toISOString()
}
};
// Test endpoint
describe('Webhook Handler', () => {
it('should process bulk.completed event', async () => {
const response = await request(app)
.post('/webhooks/email-verification')
.set('X-Webhook-Signature', generateSignature(mockPayloads.bulkCompleted))
.send(mockPayloads.bulkCompleted);
expect(response.status).toBe(200);
expect(response.body.received).toBe(true);
// Verify side effects
const job = await db('verification_jobs').where('job_id', 'job_123456').first();
expect(job.status).toBe('completed');
});
});
çµ±åãã¹ã
çœ²åæ€èšŒãå«ãå®å šãªWebhookãããŒããã¹ãããŸãã
describe('Webhook Security', () => {
it('should reject requests without signature', async () => {
const response = await request(app)
.post('/webhooks/email-verification')
.send(mockPayloads.bulkCompleted);
expect(response.status).toBe(401);
});
it('should reject requests with invalid signature', async () => {
const response = await request(app)
.post('/webhooks/email-verification')
.set('X-Webhook-Signature', 'invalid_signature')
.send(mockPayloads.bulkCompleted);
expect(response.status).toBe(401);
});
it('should accept requests with valid signature', async () => {
const signature = generateSignature(mockPayloads.bulkCompleted);
const response = await request(app)
.post('/webhooks/email-verification')
.set('X-Webhook-Signature', signature)
.send(mockPayloads.bulkCompleted);
expect(response.status).toBe(200);
});
});
BillionVerify Webhookçµ±å
BillionVerifyã¯ãã¡ãŒã«æ€èšŒã€ãã³ãçšã®å æ¬çãªWebhookãµããŒããæäŸããéåææ€èšŒã¯ãŒã¯ãããŒã®æ§ç¯ã容æã«ããŸãã
Webhookã®èšå®
BillionVerifyããã·ã¥ããŒããŸãã¯APIãä»ããŠWebhookãã»ããã¢ããããŸãã
// Register webhook via API
async function setupBillionVerifyWebhooks() {
const webhook = await registerWebhook(
'https://yourapp.com/webhooks/billionverify',
['bulk.completed', 'bulk.failed', 'bulk.progress'],
process.env.BILLIONVERIFY_WEBHOOK_SECRET
);
console.log('Webhook configured:', webhook);
}
Webhookãã€ããŒã圢åŒ
BillionVerify Webhookã«ã¯ãæ€èšŒã€ãã³ãã«é¢ããå æ¬çãªæ å ±ãå«ãŸããŠããŸãã
{
"event_type": "bulk.completed",
"webhook_id": "wh_abc123",
"job_id": "job_xyz789",
"timestamp": "2025-01-15T10:30:00Z",
"status": "completed",
"summary": {
"total": 10000,
"valid": 8500,
"invalid": 1200,
"risky": 300,
"disposable": 150,
"catch_all": 200
},
"processing_time_ms": 45000,
"download_url": "https://api.billionverify.com/v1/bulk/download/job_xyz789"
}
å®å šãªçµ±åäŸ
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Webhook endpoint for BillionVerify
app.post('/webhooks/billionverify', async (req, res) => {
// Verify signature
const signature = req.headers['x-billionverify-signature'];
const isValid = verifySignature(req.body, signature);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
processWebhookAsync(req.body);
});
async function processWebhookAsync(payload) {
try {
switch (payload.event_type) {
case 'bulk.completed':
await handleBulkCompleted(payload);
break;
case 'bulk.failed':
await handleBulkFailed(payload);
break;
case 'bulk.progress':
await handleBulkProgress(payload);
break;
}
} catch (error) {
console.error('Webhook processing error:', error);
await logFailedWebhook(payload, error);
}
}
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
ãŸãšã
ã¡ãŒã«æ€èšŒWebhookã¯ãå¹ççã§ã¹ã±ãŒã©ãã«ãã€ä¿¡é Œæ§ã®é«ãéåæåŠçãå¯èœã«ããããšã§ãã¢ããªã±ãŒã·ã§ã³ãäžæ¬æ€èšŒãåŠçããæ¹æ³ãå€é©ããŸããã»ãã¥ãªãã£å¯Ÿçããšã©ãŒåŠçãã¢ãã¿ãªã³ã°ãåããé©åãªWebhookåŠçãå®è£ ããããšã§ãã¢ããªã±ãŒã·ã§ã³ã®ããŒãºã«åãããŠã¹ã±ãŒã«ããå ç¢ãªã¡ãŒã«æ€èšŒã¯ãŒã¯ãããŒãæ§ç¯ã§ããŸãã
ã¡ãŒã«æ€èšŒWebhookãå®è£ ããããã®éèŠãªãã€ã³ãïŒ
- è¿ éã«ã¬ã¹ãã³ã¹ãããã€ããŒããéåæã«åŠçãã
- 眲åãæ€èšŒããŠãWebhookãæ£åœãªãœãŒã¹ããã®ãã®ã§ããããšã確èªãã
- åªçæ§ãå®è£ ããŠãéè€é ä¿¡ãå®å šã«åŠçãã
- ã¡ãã»ãŒãžãã¥ãŒã䜿çšããŠãå€§èŠæš¡ã§ä¿¡é Œæ§ã®é«ãåŠçãå®çŸãã
- Webhookã®å¥å šæ§ãç£èŠããã¡ããªã¯ã¹ãšã¢ã©ãŒãã䜿çšãã
æ°åãŸãã¯æ°çŸäžã®ã¡ãŒã«æ€èšŒãåŠçããå Žåã§ããWebhookã¯å¹ççãªéåæåŠçã®åºç€ãæäŸããŸããBillionVerifyã®å æ¬çãªWebhookãµããŒãã§ä»ããWebhookã®å®è£ ãéå§ããã¡ãŒã«æ€èšŒã¯ãŒã¯ãããŒã次ã®ã¬ãã«ã«åŒãäžããŸãããã