Skip to content

Commit a873cb4

Browse files
committed
fix(cli,core): fail indexing on duplicate webhook ids
Two webhook() declarations sharing an id used to silently overwrite each other in the worker manifest. Indexing now fails with the colliding ids and their file paths, matching how duplicate task ids are already handled.
1 parent d1c027f commit a873cb4

6 files changed

Lines changed: 83 additions & 0 deletions

File tree

packages/cli-v3/src/entryPoints/dev-index-worker.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { readFile } from "node:fs/promises";
1717
import sourceMapSupport from "source-map-support";
1818
import { registerResources } from "../indexing/registerResources.js";
1919
import { reportTaskIdCollisions } from "../indexing/reportTaskIdCollisions.js";
20+
import { reportWebhookIdCollisions } from "../indexing/reportWebhookIdCollisions.js";
2021
import { env } from "std-env";
2122
import { normalizeImportPath } from "../utilities/normalizeImportPath.js";
2223
import { detectRuntimeVersion } from "@trigger.dev/core/v3/build";
@@ -127,6 +128,11 @@ if (await reportTaskIdCollisions(safeSend)) {
127128
process.exit(0);
128129
}
129130

131+
if (await reportWebhookIdCollisions(safeSend)) {
132+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
133+
process.exit(0);
134+
}
135+
130136
let tasks = await convertSchemasToJsonSchemas(resourceCatalog.listTaskManifests());
131137

132138
// If the config has retry defaults, we need to apply them to all tasks that don't have any retry settings

packages/cli-v3/src/entryPoints/managed-index-worker.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { readFile } from "node:fs/promises";
1717
import sourceMapSupport from "source-map-support";
1818
import { registerResources } from "../indexing/registerResources.js";
1919
import { reportTaskIdCollisions } from "../indexing/reportTaskIdCollisions.js";
20+
import { reportWebhookIdCollisions } from "../indexing/reportWebhookIdCollisions.js";
2021
import { env } from "std-env";
2122
import { normalizeImportPath } from "../utilities/normalizeImportPath.js";
2223
import { detectRuntimeVersion } from "@trigger.dev/core/v3/build";
@@ -121,6 +122,11 @@ if (await reportTaskIdCollisions(safeSend)) {
121122
process.exit(0);
122123
}
123124

125+
if (await reportWebhookIdCollisions(safeSend)) {
126+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
127+
process.exit(0);
128+
}
129+
124130
let tasks = await convertSchemasToJsonSchemas(resourceCatalog.listTaskManifests());
125131

126132
// If the config has retry defaults, we need to apply them to all tasks that don't have any retry settings

packages/cli-v3/src/indexing/indexWorkerManifest.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { execPathForRuntime } from "@trigger.dev/core/v3/build";
22
import {
33
DuplicateTaskIdsError,
4+
DuplicateWebhookIdsError,
45
TaskIndexingImportError,
56
TaskMetadataParseError,
67
UncaughtExceptionError,
@@ -94,6 +95,13 @@ export async function indexWorkerManifest({
9495
child.kill("SIGKILL");
9596
break;
9697
}
98+
case "WEBHOOKS_FAILED_TO_INDEX": {
99+
clearTimeout(timeout);
100+
resolved = true;
101+
reject(new DuplicateWebhookIdsError(message.payload.collisions));
102+
child.kill("SIGKILL");
103+
break;
104+
}
97105
case "UNCAUGHT_EXCEPTION": {
98106
clearTimeout(timeout);
99107
resolved = true;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { indexerToWorkerMessages, resourceCatalog } from "@trigger.dev/core/v3";
2+
import { sendMessageInCatalog } from "@trigger.dev/core/v3/zodMessageHandler";
3+
4+
/**
5+
* If the indexer registered any duplicate webhook ids (across files), report
6+
* them to the parent via WEBHOOKS_FAILED_TO_INDEX and return true. Callers must
7+
* stop indexing (skip INDEX_COMPLETE) when this returns true.
8+
*/
9+
export async function reportWebhookIdCollisions(
10+
send: (message: unknown) => void
11+
): Promise<boolean> {
12+
const collisions = resourceCatalog.listWebhookIdCollisions();
13+
14+
if (collisions.length === 0) {
15+
return false;
16+
}
17+
18+
await sendMessageInCatalog(
19+
indexerToWorkerMessages,
20+
"WEBHOOKS_FAILED_TO_INDEX",
21+
{ collisions },
22+
async (msg) => {
23+
send(msg);
24+
}
25+
);
26+
27+
return true;
28+
}

packages/core/src/v3/errors.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,37 @@ export class DuplicateTaskIdsError extends Error {
606606
}
607607
}
608608

609+
function formatDuplicateWebhookIds(collisions: TaskIdCollision[]): string {
610+
const lines = collisions.map(({ id, filePaths }) => {
611+
const distinct = Array.from(new Set(filePaths));
612+
613+
if (distinct.length === 1) {
614+
return ` - "${id}" found more than once in ${distinct[0]}`;
615+
}
616+
617+
const last = distinct[distinct.length - 1];
618+
const head = distinct.slice(0, -1).join(", ");
619+
620+
return ` - "${id}" found in ${head} and ${last}`;
621+
});
622+
623+
return [
624+
"Duplicate webhook ids detected:",
625+
"",
626+
...lines,
627+
"",
628+
"Webhook ids must be unique across your project. Please rename one of them.",
629+
].join("\n");
630+
}
631+
632+
export class DuplicateWebhookIdsError extends Error {
633+
constructor(public readonly collisions: TaskIdCollision[]) {
634+
super(formatDuplicateWebhookIds(collisions));
635+
636+
this.name = "DuplicateWebhookIdsError";
637+
}
638+
}
639+
609640
export class UnexpectedExitError extends Error {
610641
constructor(
611642
public code: number,

packages/core/src/v3/schemas/messages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export const indexerToWorkerMessages = {
4343
version: z.literal("v1").default("v1"),
4444
collisions: z.array(z.object({ id: z.string(), filePaths: z.array(z.string()) })),
4545
}),
46+
WEBHOOKS_FAILED_TO_INDEX: z.object({
47+
version: z.literal("v1").default("v1"),
48+
collisions: z.array(z.object({ id: z.string(), filePaths: z.array(z.string()) })),
49+
}),
4650
UNCAUGHT_EXCEPTION: UncaughtExceptionMessage,
4751
};
4852

0 commit comments

Comments
 (0)