用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/mikailustuner/OmniRule --skill webhook-handling命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
Bun runtime: HTTP server, file I/O, SQLite, test runner, package manager, bundler — all-in-one JS toolchain.
Clerk: Drop-in auth UI, Organizations, User management, JWT templates, webhooks, Next.js middleware integration.
Gelişmiş masaüstü, tarayıcı ve işletim sistemi kontrol yeteneği. Görsel (koordinat tabanlı) fare/klavye otomasyonu, DOM manipülasyonu, pencere yönetimi, gelişmiş dosya, ağ ve süreç yönetimini kapsar.
基于 SOC 职业分类
正在显示 SKILL.md
| name | webhook-handling |
| description | Webhook Handling: Signature verification, Retry patterns, Idempotency, Event processing, Error handling. |
| triggers | {"extensions":[".ts",".js"],"directories":["webhooks/","hooks/","events/"],"keywords":["webhook","callback","event","signature","stripe","github","webhook"]} |
| auto_load_when | Building webhook endpoints or processing external events |
| agent | architect |
| tools | ["Read","Write","Bash"] |
Focus: Security, reliability, idempotency, event processing
Why Verify Signatures:
├── Prevents fake webhook requests
├── Ensures request came from expected provider
├── Protects against replay attacks
└── Mandatory for production systems
Stripe-style HMAC (Most Common):
import { createHmac } from 'crypto'
function verifyStripeSignature(payload: string, signature: string, secret: string) {
const signedPayload = `${payload}.${signature}`
const expected = createHmac('sha256', secret)
.update(signedPayload)
.digest('hex')
return signature === expected
}
// In handler:
const payload = await req.text()
const signature = req.headers.get('stripe-signature')
if (!verifyStripeSignature(payload, signature, process.env.STRIPE_SECRET)) {
return new Response('Invalid signature', { status: 401 })
}
GitHub Webhook:
import { createHmac } from 'crypto'
function verifyGitHubSignature(payload: string, signature: string, secret: string) {
const hmac = createHmac('sha256', secret)
const digest = hmac.update(payload).digest('hex')
return `sha256=${digest}` === signature
}
const signature = req.headers.get('x-hub-signature-256')
const body = await req.text()
if (!verifyGitHubSignature(body, signature, process.env.GITHUB_SECRET)) {
return new Response('Invalid signature', { status: 401 })
}
Amazon SNS Signature:
├── Verify certificate chain
├── Check topic ARN matches
├── Verify HMAC-SHA1 signature
└── Use AWS SDK: sns.verifyMessage(message)
Parse Event Payload:
app.post('/webhooks/stripe', async (c) => {
const body = await c.req.json()
const eventType = body.type
const eventData = body.data.object
})
Validate Required Fields:
function validateStripeEvent(event: any): event is StripeEvent {
return event &&
typeof event.type === 'string' &&
typeof event.data === 'object' &&
typeof event.data.object === 'object'
}
app.post('/webhooks/stripe', async (c) => {
const event = await c.req.json()
if (!validateStripeEvent(event)) {
return c.json({ error: 'Invalid event' }, 400)
}
// Process event...
})
Type-safe Event Handling:
type StripeEvent =
| { type: 'checkout.session.completed'; data: CheckoutSessionData }
| { type: 'customer.subscription.updated'; data: SubscriptionData }
| { type: 'invoice.payment_succeeded'; data: InvoiceData }
app.post('/webhooks/stripe', async (c) => {
const event = await c.req.json() as StripeEvent
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data)
break
case 'customer.subscription.updated':
await handleSubscriptionUpdate(event.data)
break
default:
console.log('Unhandled event type:', event.type)
}
})
Why Idempotency:
├── Webhooks can be sent multiple times (network retries)
├── Processing same event twice causes duplicate actions
└── Must handle duplicates gracefully
Idempotency Keys:
├── Use event ID as idempotency key (if unique)
├── Use webhook ID + timestamp
├── Or generate deterministic key from event content
Implementation:
async function processWebhook(event: WebhookEvent): Promise<void> {
const idempotencyKey = `webhook:${event.id}`
// Check if already processed
const existing = await redis.get(idempotencyKey)
if (existing) {
console.log('Duplicate webhook:', event.id)
return
}
// Process event
await processEvent(event)
// Mark as processed (with TTL)
await redis.set(idempotencyKey, 'processed', { EX: 86400 })
}
Database Idempotency:
async function processOrderWebhook(event: WebhookEvent) {
const existingOrder = await db.orders.findUnique({
where: { externalId: event.data.order_id }
})
if (existingOrder) {
return // Already created
}
await db.orders.create({
data: { ... }
})
}
Idempotency in Event Handler:
const handler = async (event: Event) => {
await withLock(`event:${event.id}`, async () => {
// Process only once
await doProcessing(event)
})
}
Webhook Retry Strategy:
├── Always respond 200 quickly (acknowledge receipt)
├── Process async (queue the work)
├── Return 200 immediately, even on failure
└── Provider handles retries on non-200
Queue-based Processing:
app.post('/webhooks/stripe', async (c) => {
const event = await c.req.json()
// Queue immediately, respond 200
await redis.lpush('webhook:queue', JSON.stringify(event))
return new Response('OK', { status: 200 })
})
// Separate worker process:
async function processQueue() {
while (true) {
const event = await redis.rpop('webhook:queue')
if (event) {
await processWebhook(JSON.parse(event))
}
}
}
Retry with Backoff:
async function processWithRetry(event: Event, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await processEvent(event)
return // Success
} catch (error) {
if (attempt === maxRetries) throw error
const delay = Math.pow(2, attempt) * 1000 // 2s, 4s
await sleep(delay)
}
}
}
Dead Letter Queue:
async function processWithDLQ(event: Event) {
try {
await processEvent(event)
} catch (error) {
// Move to DLQ after max retries
await redis.lpush('webhook:dlq', JSON.stringify({
event,
error: error.message,
attempts: event.attempts
}))
await notifySlack('Webhook failed:', event.id)
}
}
Sequential Processing:
app.post('/webhooks/stripe', async (c) => {
const event = await c.req.json()
// Process sequentially
await handleEvent(event)
return c.text('OK')
})
Parallel Processing:
app.post('/webhooks/github', async (c) => {
const event = await c.req.json()
const deliveries = event.deliveries || []
await Promise.allSettled(
deliveries.map(d => handleDelivery(d))
)
return c.text('OK')
})
Event Routing:
function routeEvent(event: WebhookEvent): EventHandler {
switch (event.source) {
case 'stripe': return handleStripe
case 'github': return handleGitHub
case 'slack': return handleSlack
case 'shopify': return handleShopify
default: return handleUnknown
}
}
app.post('/webhooks', async (c) => {
const event = await c.req.json()
const handler = routeEvent(event)
await handler(event)
})
Event Transformation:
function normalizeEvent(provider: string, payload: any): NormalizedEvent {
const base = {
id: `${provider}:${payload.id}`,
timestamp: new Date(),
provider
}
switch (provider) {
case 'stripe':
return { ...base, type: payload.type, data: payload.data.object }
case 'github':
return { ...base, type: payload.action, data: payload.repository }
default:
return base
}
}
Webhook Error Handling:
├── Return 200 for all processed events (even errors)
├── Log all failures for debugging
├── Alert on critical failures
├── Never expose internal errors in response
app.post('/webhooks/stripe', async (c) => {
try {
const event = await c.req.json()
await processWebhook(event)
return c.text('OK')
} catch (error) {
console.error('Webhook error:', error)
// Still return 200 - provider will retry
return c.text('OK')
}
})
Critical vs Non-critical:
async function processWebhook(event: WebhookEvent) {
switch (event.type) {
case 'payment.succeeded':
await handlePayment(event) // Critical - must process
break
case 'payment.method.updated':
await handlePaymentMethod(event) // Less critical
break
case 'invoice.generated':
await handleInvoice(event) // Can be processed later
break
}
}
Monitoring & Alerting:
- Track webhook processing time
- Alert on high failure rate
- Monitor retry counts
- Log all webhook requests with correlation ID
Webhook Health Check:
- Track success/failure rate per provider
- Alert if failure rate > 5%
- Monitor processing latency
- Track oldest pending webhook
Webhook Security Checklist:
├── Always verify signatures
├── Use HTTPS only
├── Validate webhook source IP (if possible)
├── Add request timeout
├── Log all webhook requests
├── Use idempotency keys
├── Implement retry logic
└── Rotate secrets regularly
Rate Limiting:
app.post('/webhooks', async (c) => {
const ip = c.req.header('x-forwarded-for')
const attempts = await redis.incr(`ratelimit:${ip}`)
if (attempts > 100) {
return c.text('Too many requests', { status: 429 })
}
await redis.expire(`ratelimit:${ip}`, 60)
})
Timeout Handling:
app.post('/webhooks/stripe', async (c) => {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
try {
await processWebhook(await c.req.json(), { signal: controller.signal })
} catch (e) {
if (e.name === 'AbortError') {
return c.text('Timeout', { status: 504 })
}
throw e
} finally {
clearTimeout(timeout)
}
return c.text('OK')
})
Secret Rotation:
- Rotate webhook secrets quarterly
- Keep old secret valid for 24h after rotation
- Update dashboard/webhook URLs immediately
- Document rotation process
❌ Processing synchronously before responding
✅ Respond 200 first, process async
❌ No signature verification
✅ Always verify HMAC signature
❌ Not handling duplicates
✅ Use idempotency keys to prevent duplicates
❌ Returning error codes (causes retries)
✅ Return 200 even on failures, handle retries via queue
❌ Exposing internal errors in response
✅ Log internally, return generic message
❌ No logging
✅ Log all webhooks with correlation ID
❌ No timeout handling
✅ Add request timeout for all webhook handlers
❌ Hardcoded secrets in code
✅ Use environment variables
| Concern | Pattern | Implementation |
|---|---|---|
| Security | HMAC signature | Verify with secret key |
| Idempotency | Event ID key | Check/set in Redis/DB |
| Reliability | Queue processing | Respond 200, queue async |
| Retry | Exponential backoff | 1s, 2s, 4s delays |
| Errors | DLQ | Move failed to dead letter |
| Timeout | AbortController | 5-10s max processing |
| Monitoring | Metrics | Track success/failure rate |