fix: optimize webhook routing (#2236)

This commit is contained in:
David Nguyen
2025-11-25 11:43:23 +11:00
committed by GitHub
parent 5df3932958
commit e364b08b6a
4 changed files with 60 additions and 70 deletions

View File

@ -1,3 +1,4 @@
// Todo: [Webhooks] delete file after deployment.
import { handlerTriggerWebhooks } from '@documenso/lib/server-only/webhooks/trigger/handler'; import { handlerTriggerWebhooks } from '@documenso/lib/server-only/webhooks/trigger/handler';
import type { Route } from './+types/webhook.trigger'; import type { Route } from './+types/webhook.trigger';

View File

@ -22,53 +22,51 @@ export const run = async ({
const { webhookUrl: url, secret } = webhook; const { webhookUrl: url, secret } = webhook;
await io.runTask('execute-webhook', async () => { const payloadData = {
const payloadData = { event,
event, payload: data,
payload: data, createdAt: new Date().toISOString(),
createdAt: new Date().toISOString(), webhookEndpoint: url,
webhookEndpoint: url, };
};
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
body: JSON.stringify(payloadData), body: JSON.stringify(payloadData),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Documenso-Secret': secret ?? '', 'X-Documenso-Secret': secret ?? '',
}, },
});
const body = await response.text();
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
try {
responseBody = JSON.parse(body);
} catch (err) {
responseBody = body;
}
await prisma.webhookCall.create({
data: {
url,
event,
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
requestBody: payloadData as Prisma.InputJsonValue,
responseCode: response.status,
responseBody,
responseHeaders: Object.fromEntries(response.headers.entries()),
webhookId: webhook.id,
},
});
if (!response.ok) {
throw new Error(`Webhook execution failed with status ${response.status}`);
}
return {
success: response.ok,
status: response.status,
};
}); });
const body = await response.text();
let responseBody: Prisma.InputJsonValue | Prisma.JsonNullValueInput = Prisma.JsonNull;
try {
responseBody = JSON.parse(body);
} catch (err) {
responseBody = body;
}
await prisma.webhookCall.create({
data: {
url,
event,
status: response.ok ? WebhookCallStatus.SUCCESS : WebhookCallStatus.FAILED,
requestBody: payloadData as Prisma.InputJsonValue,
responseCode: response.status,
responseBody,
responseHeaders: Object.fromEntries(response.headers.entries()),
webhookId: webhook.id,
},
});
if (!response.ok) {
throw new Error(`Webhook execution failed with status ${response.status}`);
}
return {
success: response.ok,
status: response.status,
};
}; };

View File

@ -13,6 +13,7 @@ export type HandlerTriggerWebhooksResponse =
error: string; error: string;
}; };
// Todo: [Webhooks] delete after deployment.
export const handlerTriggerWebhooks = async (req: Request) => { export const handlerTriggerWebhooks = async (req: Request) => {
const signature = req.headers.get('x-webhook-signature'); const signature = req.headers.get('x-webhook-signature');

View File

@ -1,7 +1,6 @@
import type { WebhookTriggerEvents } from '@prisma/client'; import type { WebhookTriggerEvents } from '@prisma/client';
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../../constants/app'; import { jobs } from '../../../jobs/client';
import { sign } from '../../crypto/sign';
import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger'; import { getAllWebhooksByEventTrigger } from '../get-all-webhooks-by-event-trigger';
export type TriggerWebhookOptions = { export type TriggerWebhookOptions = {
@ -13,35 +12,26 @@ export type TriggerWebhookOptions = {
export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWebhookOptions) => { export const triggerWebhook = async ({ event, data, userId, teamId }: TriggerWebhookOptions) => {
try { try {
const body = {
event,
data,
userId,
teamId,
};
const registeredWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId }); const registeredWebhooks = await getAllWebhooksByEventTrigger({ event, userId, teamId });
if (registeredWebhooks.length === 0) { if (registeredWebhooks.length === 0) {
return; return;
} }
const signature = sign(body); await Promise.allSettled(
registeredWebhooks.map(async (webhook) => {
await Promise.race([ await jobs.triggerJob({
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/api/webhook/trigger`, { name: 'internal.execute-webhook',
method: 'POST', payload: {
headers: { event,
'content-type': 'application/json', webhookId: webhook.id,
'x-webhook-signature': signature, data,
}, },
body: JSON.stringify(body), });
}), }),
new Promise((_, reject) => { );
setTimeout(() => reject(new Error('Request timeout')), 500);
}),
]).catch(() => null);
} catch (err) { } catch (err) {
console.error(err);
throw new Error(`Failed to trigger webhook`); throw new Error(`Failed to trigger webhook`);
} }
}; };