Skip to content
This repository was archived by the owner on May 5, 2026. It is now read-only.

Commit d2db67e

Browse files
authored
fix(cron): catch croner parse errors in cron.add and cron.update handlers (openclaw#74193)
* fix(cron): catch croner parse errors in cron.add and cron.update handlers * fix(cron): narrow catch to TypeError/RangeError only; add braces for linter
1 parent 2aa6abd commit d2db67e

2 files changed

Lines changed: 119 additions & 2 deletions

File tree

src/gateway/server-methods/cron.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,23 @@ export const cronHandlers: GatewayRequestHandlers = {
241241
);
242242
return;
243243
}
244-
const job = await context.cron.add(jobCreate);
244+
let job: Awaited<ReturnType<typeof context.cron.add>>;
245+
try {
246+
job = await context.cron.add(jobCreate);
247+
} catch (err) {
248+
if (!(err instanceof TypeError) && !(err instanceof RangeError)) {
249+
throw err;
250+
}
251+
respond(
252+
false,
253+
undefined,
254+
errorShape(
255+
ErrorCodes.INVALID_REQUEST,
256+
`invalid cron.add params: ${formatErrorMessage(err)}`,
257+
),
258+
);
259+
return;
260+
}
245261
context.logGateway.info("cron: job created", { jobId: job.id, schedule: jobCreate.schedule });
246262
respond(true, job, undefined);
247263
},
@@ -320,7 +336,23 @@ export const cronHandlers: GatewayRequestHandlers = {
320336
);
321337
return;
322338
}
323-
const job = await context.cron.update(jobId, patch);
339+
let job: Awaited<ReturnType<typeof context.cron.update>>;
340+
try {
341+
job = await context.cron.update(jobId, patch);
342+
} catch (err) {
343+
if (!(err instanceof TypeError) && !(err instanceof RangeError)) {
344+
throw err;
345+
}
346+
respond(
347+
false,
348+
undefined,
349+
errorShape(
350+
ErrorCodes.INVALID_REQUEST,
351+
`invalid cron.update params: ${formatErrorMessage(err)}`,
352+
),
353+
);
354+
return;
355+
}
324356
context.logGateway.info("cron: job updated", { jobId });
325357
respond(true, job, undefined);
326358
},

src/gateway/server-methods/cron.validation.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,89 @@ describe("cron method validation", () => {
294294
}),
295295
);
296296
});
297+
298+
it("returns INVALID_REQUEST when cron.add throws a croner parse error (#74066)", async () => {
299+
const context = createCronContext();
300+
context.cron.add.mockRejectedValueOnce(new TypeError("CronPattern: Expected 5 or 6 fields"));
301+
const respond = vi.fn();
302+
await cronHandlers["cron.add"]({
303+
req: {} as never,
304+
params: {
305+
name: "bad-cron",
306+
enabled: true,
307+
schedule: { kind: "cron", cron: "not-a-cron-expr" },
308+
sessionTarget: "isolated",
309+
wakeMode: "next-heartbeat",
310+
payload: { kind: "agentTurn", message: "ping" },
311+
} as never,
312+
respond: respond as never,
313+
context: context as never,
314+
client: null,
315+
isWebchatConnect: () => false,
316+
});
317+
318+
expect(respond).toHaveBeenCalledWith(
319+
false,
320+
undefined,
321+
expect.objectContaining({
322+
code: "INVALID_REQUEST",
323+
message: expect.stringContaining("CronPattern"),
324+
}),
325+
);
326+
});
327+
328+
it("returns INVALID_REQUEST when cron.update throws a croner parse error (#74066)", async () => {
329+
const existingJob = createCronJob();
330+
const context = createCronContext(existingJob);
331+
context.cron.update.mockRejectedValueOnce(
332+
new RangeError("CronPattern: Value out of range (99)"),
333+
);
334+
const respond = vi.fn();
335+
await cronHandlers["cron.update"]({
336+
req: {} as never,
337+
params: {
338+
id: existingJob.id,
339+
patch: {
340+
schedule: { kind: "cron", cron: "99 * * * *" },
341+
},
342+
} as never,
343+
respond: respond as never,
344+
context: context as never,
345+
client: null,
346+
isWebchatConnect: () => false,
347+
});
348+
349+
expect(respond).toHaveBeenCalledWith(
350+
false,
351+
undefined,
352+
expect.objectContaining({
353+
code: "INVALID_REQUEST",
354+
message: expect.stringContaining("CronPattern"),
355+
}),
356+
);
357+
});
358+
359+
it("re-throws non-parse errors from cron.add instead of masking as INVALID_REQUEST", async () => {
360+
const context = createCronContext();
361+
context.cron.add.mockRejectedValueOnce(new Error("DB write failed"));
362+
const respond = vi.fn();
363+
await expect(
364+
cronHandlers["cron.add"]({
365+
req: {} as never,
366+
params: {
367+
name: "db-fail",
368+
enabled: true,
369+
schedule: { kind: "every", everyMs: 60_000 },
370+
sessionTarget: "isolated",
371+
wakeMode: "next-heartbeat",
372+
payload: { kind: "agentTurn", message: "ping" },
373+
} as never,
374+
respond: respond as never,
375+
context: context as never,
376+
client: null,
377+
isWebchatConnect: () => false,
378+
}),
379+
).rejects.toThrow("DB write failed");
380+
expect(respond).not.toHaveBeenCalled();
381+
});
297382
});

0 commit comments

Comments
 (0)