Skip to content

Commit 99cce53

Browse files
fix(slack): propagate legacy webhook dispatch failures
1 parent 019061d commit 99cce53

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
@@ -529,7 +529,13 @@ describe('Webhook Trigger API Route', () => {
529529
: null
530530
})
531531
verifySlackCustomBotCredentialRequestMock.mockResolvedValue(null)
532-
dispatchSlackCustomBotCredentialMock.mockResolvedValue(1)
532+
dispatchSlackCustomBotCredentialMock.mockResolvedValue([
533+
{
534+
outcome: 'queued',
535+
reason: 'queued',
536+
response: new NextResponse(null, { status: 200 }),
537+
},
538+
])
533539

534540
// Set up default workflow for tests
535541
testData.workflows.push({
@@ -760,6 +766,36 @@ describe('Webhook Trigger API Route', () => {
760766
expect(dispatchSlackCustomBotCredentialMock).not.toHaveBeenCalled()
761767
expect(dispatchResolvedWebhookTargetMock).not.toHaveBeenCalled()
762768
})
769+
770+
it('propagates a legacy fan-out failure when no target queues successfully', async () => {
771+
testData.webhooks.push({
772+
id: 'legacy-slack-webhook',
773+
provider: 'slack',
774+
path: 'legacy-slack-path',
775+
routingKey: 'credential-1',
776+
isActive: true,
777+
providerConfig: {
778+
triggerId: 'slack_webhook',
779+
credentialId: 'credential-1',
780+
ingressMode: 'legacy_custom_bot',
781+
},
782+
workflowId: 'test-workflow-id',
783+
})
784+
dispatchSlackCustomBotCredentialMock.mockResolvedValueOnce([
785+
{
786+
outcome: 'failed',
787+
reason: 'preprocessing',
788+
response: NextResponse.json({ error: 'Preprocessing failed' }, { status: 500 }),
789+
},
790+
])
791+
792+
const response = await POST(createMockRequest('POST', { type: 'event_callback' }), {
793+
params: Promise.resolve({ path: 'legacy-slack-path' }),
794+
})
795+
796+
expect(response.status).toBe(500)
797+
expect(dispatchResolvedWebhookTargetMock).not.toHaveBeenCalled()
798+
})
763799
})
764800

765801
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
@@ -13,6 +13,7 @@ import {
1313
handleProviderReachabilityTest,
1414
parseWebhookBody,
1515
verifyProviderAuth,
16+
type WebhookDispatchResult,
1617
} from '@/lib/webhooks/processor'
1718
import { acceptsPathWebhookDelivery } from '@/lib/webhooks/providers'
1819
import {
@@ -142,8 +143,9 @@ async function handleWebhookPost(
142143
)
143144
}
144145

145-
let dispatchedLegacySlackAlias = false
146+
let authenticatedLegacySlackAlias = false
146147
let firstLegacySlackAuthError: NextResponse | null = null
148+
const legacySlackDispatchResults: WebhookDispatchResult[] = []
147149
for (const credentialId of legacySlackCredentialIds) {
148150
const authError = await verifySlackCustomBotCredentialRequest({
149151
credentialId,
@@ -157,17 +159,18 @@ async function handleWebhookPost(
157159
continue
158160
}
159161

160-
await dispatchSlackCustomBotCredential({
162+
const dispatchResults = await dispatchSlackCustomBotCredential({
161163
credentialId,
162164
body,
163165
request,
164166
requestId,
165167
receivedAt,
166168
})
167-
dispatchedLegacySlackAlias = true
169+
authenticatedLegacySlackAlias = true
170+
legacySlackDispatchResults.push(...dispatchResults)
168171
}
169172

170-
if (legacySlackCredentialIds.size > 0 && !dispatchedLegacySlackAlias) {
173+
if (legacySlackCredentialIds.size > 0 && !authenticatedLegacySlackAlias) {
171174
return (
172175
firstLegacySlackAuthError ??
173176
new NextResponse('Unauthorized - Invalid Slack signature', { status: 401 })
@@ -178,11 +181,17 @@ async function handleWebhookPost(
178181
* Process each unmarked webhook matched on this path. Marked Slack rows were
179182
* already included in the routing-key fan-out and must not run twice.
180183
*/
181-
const responses: NextResponse[] = dispatchedLegacySlackAlias
182-
? [new NextResponse(null, { status: 200 })]
183-
: []
184+
const responses: NextResponse[] = []
184185
const failures: NextResponse[] = []
185-
const dispatchTargetCount = directWebhooksForPath.length + (dispatchedLegacySlackAlias ? 1 : 0)
186+
for (const dispatchResult of legacySlackDispatchResults) {
187+
if (dispatchResult.reason === 'filtered') continue
188+
if (dispatchResult.outcome === 'failed' || dispatchResult.reason === 'block-missing') {
189+
failures.push(dispatchResult.response)
190+
continue
191+
}
192+
responses.push(dispatchResult.response)
193+
}
194+
const dispatchTargetCount = directWebhooksForPath.length + legacySlackDispatchResults.length
186195

187196
for (const { webhook: foundWebhook, workflow: foundWorkflow } of directWebhooksForPath) {
188197
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)