Skip to content

Commit 82f82fa

Browse files
committed
fix(triggers): dedup + not surfacing deployment status log
1 parent 6f3dee8 commit 82f82fa

4 files changed

Lines changed: 120 additions & 31 deletions

File tree

apps/sim/app/api/webhooks/trigger/[path]/route.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { type NextRequest, NextResponse } from 'next/server'
2-
import { v4 as uuidv4 } from 'uuid'
32
import { createLogger } from '@/lib/logs/console/logger'
4-
import { LoggingSession } from '@/lib/logs/execution/logging-session'
53
import { generateRequestId } from '@/lib/utils'
64
import {
75
checkRateLimits,
@@ -139,34 +137,10 @@ export async function POST(
139137
if (foundWebhook.blockId) {
140138
const blockExists = await blockExistsInDeployment(foundWorkflow.id, foundWebhook.blockId)
141139
if (!blockExists) {
142-
logger.warn(
140+
logger.info(
143141
`[${requestId}] Trigger block ${foundWebhook.blockId} not found in deployment for workflow ${foundWorkflow.id}`
144142
)
145-
146-
const executionId = uuidv4()
147-
const loggingSession = new LoggingSession(foundWorkflow.id, executionId, 'webhook', requestId)
148-
149-
const actorUserId = foundWorkflow.workspaceId
150-
? (await import('@/lib/workspaces/utils')).getWorkspaceBilledAccountUserId(
151-
foundWorkflow.workspaceId
152-
) || foundWorkflow.userId
153-
: foundWorkflow.userId
154-
155-
await loggingSession.safeStart({
156-
userId: actorUserId,
157-
workspaceId: foundWorkflow.workspaceId || '',
158-
variables: {},
159-
})
160-
161-
await loggingSession.safeCompleteWithError({
162-
error: {
163-
message: `Trigger block not deployed. The webhook trigger (block ${foundWebhook.blockId}) is not present in the deployed workflow. Please redeploy the workflow.`,
164-
stackTrace: undefined,
165-
},
166-
traceSpans: [],
167-
})
168-
169-
return new NextResponse('Trigger block not deployed', { status: 404 })
143+
return new NextResponse('Trigger block not found in deployment', { status: 404 })
170144
}
171145
}
172146

apps/sim/background/webhook-execution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
112112

113113
const idempotencyKey = IdempotencyService.createWebhookIdempotencyKey(
114114
payload.webhookId,
115-
payload.headers
115+
payload.headers,
116+
payload.body,
117+
payload.provider
116118
)
117119

118120
const runOperation = async () => {

apps/sim/lib/idempotency/service.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { idempotencyKey } from '@sim/db/schema'
44
import { and, eq } from 'drizzle-orm'
55
import { createLogger } from '@/lib/logs/console/logger'
66
import { getRedisClient } from '@/lib/redis'
7+
import { extractProviderIdentifierFromBody } from '@/lib/webhooks/provider-utils'
78

89
const logger = createLogger('IdempotencyService')
910

@@ -451,13 +452,25 @@ export class IdempotencyService {
451452

452453
/**
453454
* Create an idempotency key from a webhook payload following RFC best practices
454-
* Standard webhook headers (webhook-id, x-webhook-id, etc.)
455+
* Checks both headers and body for unique identifiers to prevent duplicate executions
456+
*
457+
* @param webhookId - The webhook database ID
458+
* @param headers - HTTP headers from the webhook request
459+
* @param body - Parsed webhook body (optional, used for provider-specific identifiers)
460+
* @param provider - Provider name for body extraction (optional)
461+
* @returns A unique idempotency key for this webhook event
455462
*/
456-
static createWebhookIdempotencyKey(webhookId: string, headers?: Record<string, string>): string {
463+
static createWebhookIdempotencyKey(
464+
webhookId: string,
465+
headers?: Record<string, string>,
466+
body?: any,
467+
provider?: string
468+
): string {
457469
const normalizedHeaders = headers
458470
? Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]))
459471
: undefined
460472

473+
// Check standard webhook headers first
461474
const webhookIdHeader =
462475
normalizedHeaders?.['webhook-id'] ||
463476
normalizedHeaders?.['x-webhook-id'] ||
@@ -470,7 +483,22 @@ export class IdempotencyService {
470483
return `${webhookId}:${webhookIdHeader}`
471484
}
472485

486+
// Check body for provider-specific unique identifiers
487+
if (body && provider) {
488+
const bodyIdentifier = extractProviderIdentifierFromBody(provider, body)
489+
490+
if (bodyIdentifier) {
491+
return `${webhookId}:${bodyIdentifier}`
492+
}
493+
}
494+
495+
// No unique identifier found - generate random UUID
496+
// This means duplicate detection will not work for this webhook
473497
const uniqueId = randomUUID()
498+
logger.warn('No unique identifier found, duplicate executions may occur', {
499+
webhookId,
500+
provider,
501+
})
474502
return `${webhookId}:${uniqueId}`
475503
}
476504
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Provider-specific unique identifier extractors for webhook idempotency
3+
*/
4+
5+
function extractSlackIdentifier(body: any): string | null {
6+
if (body.event_id) {
7+
return body.event_id
8+
}
9+
10+
if (body.event?.ts && body.team_id) {
11+
return `${body.team_id}:${body.event.ts}`
12+
}
13+
14+
return null
15+
}
16+
17+
function extractTwilioIdentifier(body: any): string | null {
18+
return body.MessageSid || body.CallSid || null
19+
}
20+
21+
function extractStripeIdentifier(body: any): string | null {
22+
if (body.id && body.object === 'event') {
23+
return body.id
24+
}
25+
return null
26+
}
27+
28+
function extractHubSpotIdentifier(body: any): string | null {
29+
if (Array.isArray(body) && body.length > 0 && body[0]?.eventId) {
30+
return String(body[0].eventId)
31+
}
32+
return null
33+
}
34+
35+
function extractLinearIdentifier(body: any): string | null {
36+
if (body.action && body.data?.id) {
37+
return `${body.action}:${body.data.id}`
38+
}
39+
return null
40+
}
41+
42+
function extractJiraIdentifier(body: any): string | null {
43+
if (body.webhookEvent && (body.issue?.id || body.project?.id)) {
44+
return `${body.webhookEvent}:${body.issue?.id || body.project?.id}`
45+
}
46+
return null
47+
}
48+
49+
function extractMicrosoftTeamsIdentifier(body: any): string | null {
50+
if (body.value && Array.isArray(body.value) && body.value.length > 0) {
51+
const notification = body.value[0]
52+
if (notification.subscriptionId && notification.resourceData?.id) {
53+
return `${notification.subscriptionId}:${notification.resourceData.id}`
54+
}
55+
}
56+
return null
57+
}
58+
59+
function extractAirtableIdentifier(body: any): string | null {
60+
if (body.cursor && typeof body.cursor === 'string') {
61+
return body.cursor
62+
}
63+
return null
64+
}
65+
66+
const PROVIDER_EXTRACTORS: Record<string, (body: any) => string | null> = {
67+
slack: extractSlackIdentifier,
68+
twilio: extractTwilioIdentifier,
69+
twilio_voice: extractTwilioIdentifier,
70+
stripe: extractStripeIdentifier,
71+
hubspot: extractHubSpotIdentifier,
72+
linear: extractLinearIdentifier,
73+
jira: extractJiraIdentifier,
74+
microsoftteams: extractMicrosoftTeamsIdentifier,
75+
airtable: extractAirtableIdentifier,
76+
}
77+
78+
export function extractProviderIdentifierFromBody(provider: string, body: any): string | null {
79+
if (!body || typeof body !== 'object') {
80+
return null
81+
}
82+
83+
const extractor = PROVIDER_EXTRACTORS[provider]
84+
return extractor ? extractor(body) : null
85+
}

0 commit comments

Comments
 (0)