Skip to content

Commit fc02c3c

Browse files
fix(slack): propagate legacy webhook dispatch failures
1 parent 9a2da7e commit fc02c3c

4 files changed

Lines changed: 64 additions & 15 deletions

File tree

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,13 @@ describe('Webhook Trigger API Route', () => {
520520
: null
521521
})
522522
verifySlackCustomBotCredentialRequestMock.mockResolvedValue(null)
523-
dispatchSlackCustomBotCredentialMock.mockResolvedValue(1)
523+
dispatchSlackCustomBotCredentialMock.mockResolvedValue([
524+
{
525+
outcome: 'queued',
526+
reason: 'queued',
527+
response: new NextResponse(null, { status: 200 }),
528+
},
529+
])
524530

525531
// Set up default workflow for tests
526532
testData.workflows.push({
@@ -1029,6 +1035,36 @@ describe('Webhook Trigger API Route', () => {
10291035
expect(dispatchSlackCustomBotCredentialMock).not.toHaveBeenCalled()
10301036
expect(dispatchResolvedWebhookTargetMock).not.toHaveBeenCalled()
10311037
})
1038+
1039+
it('propagates a legacy fan-out failure when no target queues successfully', async () => {
1040+
testData.webhooks.push({
1041+
id: 'legacy-slack-webhook',
1042+
provider: 'slack',
1043+
path: 'legacy-slack-path',
1044+
routingKey: 'credential-1',
1045+
isActive: true,
1046+
providerConfig: {
1047+
triggerId: 'slack_webhook',
1048+
credentialId: 'credential-1',
1049+
ingressMode: 'legacy_custom_bot',
1050+
},
1051+
workflowId: 'test-workflow-id',
1052+
})
1053+
dispatchSlackCustomBotCredentialMock.mockResolvedValueOnce([
1054+
{
1055+
outcome: 'failed',
1056+
reason: 'preprocessing',
1057+
response: NextResponse.json({ error: 'Preprocessing failed' }, { status: 500 }),
1058+
},
1059+
])
1060+
1061+
const response = await POST(createMockRequest('POST', { type: 'event_callback' }), {
1062+
params: Promise.resolve({ path: 'legacy-slack-path' }),
1063+
})
1064+
1065+
expect(response.status).toBe(500)
1066+
expect(dispatchResolvedWebhookTargetMock).not.toHaveBeenCalled()
1067+
})
10321068
})
10331069

10341070
describe('Reservation-free filtering', () => {

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
handleProviderReachabilityTest,
2020
parseWebhookBody,
2121
verifyProviderAuth,
22+
type WebhookDispatchResult,
2223
} from '@/lib/webhooks/processor'
2324
import { acceptsPathWebhookDelivery, acceptsWebhookDeliveryMethod } from '@/lib/webhooks/providers'
2425
import {
@@ -216,8 +217,9 @@ async function handleWebhookDelivery(
216217
)
217218
}
218219

219-
let dispatchedLegacySlackAlias = false
220+
let authenticatedLegacySlackAlias = false
220221
let firstLegacySlackAuthError: NextResponse | null = null
222+
const legacySlackDispatchResults: WebhookDispatchResult[] = []
221223
for (const credentialId of legacySlackCredentialIds) {
222224
const authError = await verifySlackCustomBotCredentialRequest({
223225
credentialId,
@@ -231,17 +233,18 @@ async function handleWebhookDelivery(
231233
continue
232234
}
233235

234-
await dispatchSlackCustomBotCredential({
236+
const dispatchResults = await dispatchSlackCustomBotCredential({
235237
credentialId,
236238
body,
237239
request,
238240
requestId,
239241
receivedAt,
240242
})
241-
dispatchedLegacySlackAlias = true
243+
authenticatedLegacySlackAlias = true
244+
legacySlackDispatchResults.push(...dispatchResults)
242245
}
243246

244-
if (legacySlackCredentialIds.size > 0 && !dispatchedLegacySlackAlias) {
247+
if (legacySlackCredentialIds.size > 0 && !authenticatedLegacySlackAlias) {
245248
return (
246249
firstLegacySlackAuthError ??
247250
new NextResponse('Unauthorized - Invalid Slack signature', { status: 401 })
@@ -252,11 +255,17 @@ async function handleWebhookDelivery(
252255
* Process each unmarked webhook matched on this path. Marked Slack rows were
253256
* already included in the routing-key fan-out and must not run twice.
254257
*/
255-
const responses: NextResponse[] = dispatchedLegacySlackAlias
256-
? [new NextResponse(null, { status: 200 })]
257-
: []
258+
const responses: NextResponse[] = []
258259
const failures: NextResponse[] = []
259-
const dispatchTargetCount = directWebhooksForPath.length + (dispatchedLegacySlackAlias ? 1 : 0)
260+
for (const dispatchResult of legacySlackDispatchResults) {
261+
if (dispatchResult.reason === 'filtered') continue
262+
if (dispatchResult.outcome === 'failed' || dispatchResult.reason === 'block-missing') {
263+
failures.push(dispatchResult.response)
264+
continue
265+
}
266+
responses.push(dispatchResult.response)
267+
}
268+
const dispatchTargetCount = directWebhooksForPath.length + legacySlackDispatchResults.length
260269

261270
for (const { webhook: foundWebhook, workflow: foundWorkflow } of directWebhooksForPath) {
262271
const provider = foundWebhook.provider

apps/sim/lib/webhooks/slack-custom-ingress.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
22
import type { NextRequest } from 'next/server'
33
import { NextResponse } from 'next/server'
44
import { getSlackBotCredential } from '@/lib/oauth/credential-service'
5-
import { findWebhooksByRoutingKey } from '@/lib/webhooks/processor'
5+
import { findWebhooksByRoutingKey, type WebhookDispatchResult } from '@/lib/webhooks/processor'
66
import { verifySlackRequestSignature } from '@/lib/webhooks/providers/slack'
77
import { LEGACY_SLACK_CUSTOM_BOT_INGRESS_MODE } from '@/lib/webhooks/slack-custom-ingress-constants'
88
import { dispatchSlackWebhooks } from '@/lib/webhooks/slack-dispatch'
@@ -99,15 +99,14 @@ export async function dispatchSlackCustomBotCredential({
9999
request,
100100
requestId,
101101
receivedAt,
102-
}: DispatchSlackCustomBotOptions): Promise<number> {
102+
}: DispatchSlackCustomBotOptions): Promise<WebhookDispatchResult[]> {
103103
const webhooks = await findWebhooksByRoutingKey(credentialId, requestId, 'slack')
104104
if (webhooks.length === 0) {
105105
logger.info(
106106
`[${requestId}] No active trigger for bot credential ${credentialId}; nothing to run`
107107
)
108-
return 0
108+
return []
109109
}
110110

111-
await dispatchSlackWebhooks(webhooks, { body, request, requestId, receivedAt })
112-
return webhooks.length
111+
return dispatchSlackWebhooks(webhooks, { body, request, requestId, receivedAt })
113112
}

apps/sim/lib/webhooks/slack-dispatch.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { NextRequest } from 'next/server'
33
import {
44
dispatchResolvedWebhookTarget,
55
type findWebhooksByRoutingKey,
6+
type WebhookDispatchResult,
67
} from '@/lib/webhooks/processor'
78
import { resolveSlackEventKey } from '@/lib/webhooks/providers/slack'
89

@@ -25,11 +26,12 @@ interface DispatchSlackWebhooksOptions {
2526
export async function dispatchSlackWebhooks(
2627
webhooks: Awaited<ReturnType<typeof findWebhooksByRoutingKey>>,
2728
{ body, request, requestId, receivedAt }: DispatchSlackWebhooksOptions
28-
): Promise<void> {
29+
): Promise<WebhookDispatchResult[]> {
2930
const payload = body as Record<string, unknown>
3031
const slackRequestTimestamp = request.headers.get('x-slack-request-timestamp')
3132
const parsedTimestampMs = slackRequestTimestamp ? Number(slackRequestTimestamp) * 1000 : undefined
3233
const triggerTimestampMs = Number.isFinite(parsedTimestampMs) ? parsedTimestampMs : undefined
34+
const results: WebhookDispatchResult[] = []
3335

3436
for (const { webhook: foundWebhook, workflow: foundWorkflow } of webhooks) {
3537
const result = await dispatchResolvedWebhookTarget(foundWebhook, foundWorkflow, body, request, {
@@ -52,5 +54,8 @@ export async function dispatchSlackWebhooks(
5254
botId: rawEvent?.bot_id,
5355
})
5456
}
57+
results.push(result)
5558
}
59+
60+
return results
5661
}

0 commit comments

Comments
 (0)