Skip to content

Commit 7029ee8

Browse files
authored
Merge branch 'main' into docs-ai-agents-chat-agent-guide
2 parents 7c39f65 + 7246f67 commit 7029ee8

9 files changed

Lines changed: 636 additions & 2 deletions

File tree

.server-changes/deeplink-routes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Short links like /_/apikeys now take you straight to that page in your current project and environment, so you no longer need the full URL with your org, project and environment in it.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Fixed a rare error where triggering a task could fail if the idempotency key or debounce key contained an invalid null character. The character is now removed automatically and the run is created as normal.

apps/webapp/app/routes/[_].$.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { prisma } from "~/db.server";
3+
import { getUsersInvites } from "~/models/member.server";
4+
import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server";
5+
import { requireUser } from "~/services/session.server";
6+
import { deeplinkSuffix, resolveDeeplinkPage } from "~/utils/deeplinkPages";
7+
import {
8+
invitesPath,
9+
newOrganizationPath,
10+
newProjectPath,
11+
v3EnvironmentPath,
12+
} from "~/utils/pathBuilder";
13+
14+
//`[_]` escapes the underscore: an unescaped `_.$` is a pathless layout, mounted at `/*`.
15+
export const loader = async ({ request }: LoaderFunctionArgs) => {
16+
const user = await requireUser(request);
17+
18+
const { pathname, search } = new URL(request.url);
19+
const page = resolveDeeplinkPage(deeplinkSuffix(pathname));
20+
21+
const invites = await getUsersInvites({ email: user.email });
22+
if (invites.length > 0) {
23+
return redirect(invitesPath());
24+
}
25+
26+
const presenter = new SelectBestEnvironmentPresenter();
27+
try {
28+
const { project, organization, environment } = await presenter.call({ user });
29+
const environmentPath = v3EnvironmentPath(organization, project, environment);
30+
31+
const suffix = page ? `/${page}` : "";
32+
33+
return redirect(`${environmentPath}${suffix}${search}`);
34+
} catch (_e) {
35+
const organization = await prisma.organization.findFirst({
36+
where: {
37+
members: {
38+
some: {
39+
userId: user.id,
40+
},
41+
},
42+
deletedAt: null,
43+
},
44+
orderBy: {
45+
createdAt: "desc",
46+
},
47+
});
48+
49+
if (organization) {
50+
return redirect(newProjectPath(organization));
51+
}
52+
53+
return redirect(newOrganizationPath());
54+
}
55+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, expect, vi } from "vitest";
2+
3+
vi.mock("~/db.server", () => ({
4+
prisma: {},
5+
$replica: {},
6+
runOpsNewPrisma: {},
7+
runOpsLegacyPrisma: {},
8+
runOpsNewReplica: {},
9+
runOpsLegacyReplica: {},
10+
}));
11+
vi.mock("~/v3/runOpsMigration/splitMode.server", () => ({ isSplitEnabled: async () => false }));
12+
vi.mock("~/services/platform.v3.server", async (importOriginal) => {
13+
const actual = (await importOriginal()) as Record<string, unknown>;
14+
return {
15+
...actual,
16+
getEntitlement: vi.fn(),
17+
};
18+
});
19+
20+
import { setupAuthenticatedEnvironment } from "@internal/run-engine/tests";
21+
import { assertNonNullable, containerTest } from "@internal/testcontainers";
22+
import { trace } from "@opentelemetry/api";
23+
import { IdempotencyKeyConcern } from "~/runEngine/concerns/idempotencyKeys.server";
24+
import { DefaultQueueManager } from "~/runEngine/concerns/queues.server";
25+
import { RunEngineTriggerTaskService } from "./triggerTask.server";
26+
import {
27+
buildEngine,
28+
CapturingParentRunValidator,
29+
MockPayloadProcessor,
30+
MockTraceEventConcern,
31+
} from "./triggerTask.server.test.helpers";
32+
33+
vi.setConfig({ testTimeout: 60_000 });
34+
35+
const NUL = String.fromCharCode(0);
36+
37+
function buildService(engine: any, prisma: any) {
38+
return new RunEngineTriggerTaskService({
39+
engine,
40+
prisma,
41+
payloadProcessor: new MockPayloadProcessor(),
42+
queueConcern: new DefaultQueueManager(prisma, engine),
43+
idempotencyKeyConcern: new IdempotencyKeyConcern(prisma, engine, new MockTraceEventConcern()),
44+
validator: new CapturingParentRunValidator(),
45+
traceEventConcern: new MockTraceEventConcern(),
46+
tracer: trace.getTracer("test", "0.0.0"),
47+
metadataMaximumSize: 1024 * 1024 * 1,
48+
});
49+
}
50+
51+
describe("RunEngineTriggerTaskService null-byte sanitization", () => {
52+
containerTest(
53+
"strips a NUL from idempotencyKeyOptions.key so the jsonb insert does not 22P05",
54+
async ({ prisma, redisOptions }) => {
55+
const engine = buildEngine(prisma, redisOptions);
56+
57+
try {
58+
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
59+
const service = buildService(engine, prisma);
60+
61+
const result = await service.call({
62+
taskId: "nul-idem-task",
63+
environment,
64+
body: {
65+
payload: { kind: "idem" },
66+
options: {
67+
idempotencyKey: "a".repeat(64),
68+
idempotencyKeyOptions: { key: `acme${NUL}inc`, scope: "run" },
69+
},
70+
},
71+
});
72+
assertNonNullable(result);
73+
74+
const row = await prisma.taskRun.findUniqueOrThrow({ where: { id: result.run.id } });
75+
expect(row.idempotencyKeyOptions).toEqual({ key: "acmeinc", scope: "run" });
76+
} finally {
77+
await engine.quit();
78+
}
79+
}
80+
);
81+
82+
containerTest(
83+
"strips a NUL from debounce.key so the jsonb insert does not 22P05",
84+
async ({ prisma, redisOptions }) => {
85+
const engine = buildEngine(prisma, redisOptions);
86+
87+
try {
88+
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
89+
const service = buildService(engine, prisma);
90+
91+
const result = await service.call({
92+
taskId: "nul-debounce-task",
93+
environment,
94+
body: {
95+
payload: { kind: "debounce" },
96+
options: {
97+
debounce: { key: `grp${NUL}1`, delay: "1s" },
98+
},
99+
},
100+
});
101+
assertNonNullable(result);
102+
103+
const row = await prisma.taskRun.findUniqueOrThrow({ where: { id: result.run.id } });
104+
expect((row.debounce as { key: string }).key).toBe("grp1");
105+
} finally {
106+
await engine.quit();
107+
}
108+
}
109+
);
110+
});

apps/webapp/app/runEngine/services/triggerTask.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { PrismaClientOrTransaction } from "@trigger.dev/database";
2525
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
2626
import { logger } from "~/services/logger.server";
2727
import { parseDelay } from "~/utils/delays";
28+
import { removeNullBytesFromKey } from "~/utils/nullBytes";
2829
import { handleMetadataPacket } from "~/utils/packets";
2930
import { startSpan } from "~/v3/tracing.server";
3031
import { resolveRunIdMintKind } from "~/v3/engineVersion.server";
@@ -906,7 +907,7 @@ export class RunEngineTriggerTaskService {
906907
environment: args.environment,
907908
idempotencyKey: args.idempotencyKey,
908909
idempotencyKeyExpiresAt: args.idempotencyKey ? args.idempotencyKeyExpiresAt : undefined,
909-
idempotencyKeyOptions: args.body.options?.idempotencyKeyOptions,
910+
idempotencyKeyOptions: removeNullBytesFromKey(args.body.options?.idempotencyKeyOptions),
910911
taskIdentifier: args.taskId,
911912
payload: args.payloadPacket.data ?? "",
912913
payloadType: args.payloadPacket.dataType,
@@ -971,7 +972,7 @@ export class RunEngineTriggerTaskService {
971972
planType: args.planType,
972973
realtimeStreamsVersion: args.options.realtimeStreamsVersion,
973974
streamBasinName: args.environment.organization.streamBasinName,
974-
debounce: args.body.options?.debounce,
975+
debounce: removeNullBytesFromKey(args.body.options?.debounce),
975976
annotations: args.annotations,
976977
};
977978
}

0 commit comments

Comments
 (0)