Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
Server-side cron pattern validation is missing in the gateway handlers for cron.add and cron.update. When a user passes an invalid --cron expression to openclaw cron add or openclaw cron edit, the croner library throws a raw TypeError (bad shape) or RangeError (out-of-range field) from inside context.cron.add(...) / context.cron.update(...) (after the handler's INVALID_REQUEST guards have already passed). The throw escapes the handler and is caught by the WS dispatcher's catch-all, which (a) logs it at error level as request handler failed: TypeError|RangeError: CronPattern: ... and (b) maps it onto ErrorCodes.UNAVAILABLE (a code that means "service unavailable, retry later") instead of INVALID_REQUEST. Operators get false-positive ERROR-level entries that look like real gateway crashes, and the CLI displays a raw GatewayClientRequestError: TypeError: ... that leaks JS error class names. The matching --every value path is validated cleanly client-side and produces Error: Invalid --every; use e.g. 10m, 1h, 1d, so this is a server-side gap, not a CLI parser gap.
Steps to reproduce
-
Have an OpenClaw gateway running locally (the default loopback gateway is fine; no live channels/providers needed): pnpm openclaw status | grep "Gateway " | head -1 → "Gateway | local · ws://127.0.0.1:18789 ..."
-
Reproduce both shapes of croner rejection (one per error class):
- TypeError shape (wrong field count):
pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping"
- RangeError shape (out-of-range minute):
pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping"
- RangeError on edit too:
pnpm openclaw cron add --name probetest --every 1h --message "ping" --json (capture id); pnpm openclaw cron edit <id> --cron "* * * 13 *" (invalid month)
-
Inspect the gateway log: pnpm openclaw logs --max-bytes 60000 | grep -E "request handler failed|errorCode=UNAVAILABLE.*cron|GatewayClientRequestError.*Cron"
-
Compare with the parallel --every path which IS validated cleanly: pnpm openclaw cron add --name probetest --every "BogusDuration" --message "ping" → "Error: Invalid --every; use e.g. 10m, 1h, 1d" (No gateway log entry, exit 1, clean.)
Expected behavior
Bad --cron syntax should land on the same UX as bad --every:
(a) Gateway should classify cron-pattern parse failures as user-input rejections, not handler crashes. The handler must return ErrorCodes.INVALID_REQUEST with a human-readable message such as "invalid --cron: ". Concrete grounded reference: the sibling INVALID_REQUEST guards already in the same file at src/gateway/server-methods/cron.ts:200-208 (normalizeCronJobCreate), :211-219 (validateCronAddParams), :225-230 (validateScheduleTimestamp), :234-243 (assertValidCronCreateDelivery), :253-262, :267-275, :297-303, :313-322 — they all wrap throwing validators in try/catch and respond with INVALID_REQUEST. The remaining call to context.cron.add(jobCreate) on line 245 and context.cron.update(jobId, patch) on line 324 are not wrapped, so any throw from croner escapes.
(b) Gateway should log such rejections at info (or at most warn) on the cron subsystem, not at error on the gateway subsystem. The current catch-all in src/gateway/server/ws-connection/message-handler.ts:1586-1589 treats every uncaught handler throw as error + UNAVAILABLE, which is correct for true unexpected failures but wrong for known-class user-input throws.
(c) CLI should never display GatewayClientRequestError: TypeError: ... / GatewayClientRequestError: RangeError: ... — those error class names leak runtime internals. The matching --every path produces a clean Error: Invalid --every; use e.g. 10m, 1h, 1d. --cron should produce something equivalent, e.g. Error: Invalid --cron: <pattern>: <reason>.
The errorCode in the WS protocol response should be INVALID_REQUEST per the existing enum at src/gateway/protocol/schema/error-codes.ts:7, not UNAVAILABLE (which means "service unavailable" with retry semantics).
Actual behavior
Three layered defects, all triggered by a single bad --cron value.
Defect 1 — server-side validation gap. src/gateway/server-methods/cron.ts:188-247 ("cron.add") guards normalizeCronJobCreate, validateCronAddParams, validateScheduleTimestamp, and assertValidCronCreateDelivery with try/catch + INVALID_REQUEST. But the final call await context.cron.add(jobCreate) at line 245 is NOT wrapped. The croner library throws TypeError/RangeError during pattern parsing inside that call, and the throw escapes the handler. Same gap on line 324 for await context.cron.update(jobId, patch) (cron.update handler).
Defect 2 — wrong protocol error code. The WS dispatcher catch-all at src/gateway/server/ws-connection/message-handler.ts:1586-1589 maps every uncaught handler throw to: logGateway.error(request handler failed: ${formatForLog(err)}); respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err))); So invalid user input ends up as errorCode=UNAVAILABLE, which per src/gateway/protocol/schema/error-codes.ts is reserved for service-unavailable states with retry semantics — not user input rejection.
Defect 3 — log noise + class-name leak in CLI output. The same dispatcher logs at error on the gateway subsystem with "request handler failed: TypeError|RangeError" — exactly the line shape operators scan for to spot real handler crashes. The CLI relays the raw error string back as GatewayClientRequestError: TypeError: CronPattern: ..., leaking JS class names.
Live evidence (2026-04-29, on commit bfdee7c8):
$ pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping"
GatewayClientRequestError: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
ELIFECYCLE Command failed with exit code 1.
$ pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping"
GatewayClientRequestError: RangeError: CronPattern: Invalid value for minute: 99
ELIFECYCLE Command failed with exit code 1.
$ pnpm openclaw cron edit <id> --cron "* * * 13 *"
GatewayClientRequestError: RangeError: CronPattern: Invalid value for month: 12
ELIFECYCLE Command failed with exit code 1.
$ pnpm openclaw logs --max-bytes 60000 | grep -E "request handler failed|errorCode=UNAVAILABLE.*cron"
2026-04-29T03:23:31.476Z error gateway request handler failed: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:31.480Z info gateway/ws ⇄ res ✗ cron.add 22ms errorCode=UNAVAILABLE errorMessage=TypeError: CronPattern: invalid configuration format ...
2026-04-29T03:23:37.686Z error gateway request handler failed: RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:23:37.693Z info gateway/ws ⇄ res ✗ cron.add 11ms errorCode=UNAVAILABLE errorMessage=RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:19:12.760Z error gateway request handler failed: RangeError: CronPattern: Invalid value for month: 12
2026-04-29T03:19:12.764Z info gateway/ws ⇄ res ✗ cron.update 7ms errorCode=UNAVAILABLE errorMessage=RangeError: CronPattern: Invalid value for month: 12
Compare: parallel --every value path validated cleanly client-side; no gateway log entry produced.
$ pnpm openclaw cron add --name probetest --every "BogusDuration" --message "ping"
Error: Invalid --every; use e.g. 10m, 1h, 1d
ELIFECYCLE Command failed with exit code 1.
### OpenClaw version
2026.4.27 (bfdee7c)
### Operating system
Ubuntu 24.04.4 LTS (Linux 6.8.0-110-generic)
### Install method
pnpm dev (repo checkout, `pnpm openclaw ...`)
### Model
anthropic/claude-opus-4-7
### Provider / routing chain
N/A
### Additional provider/model setup details
_No response_
### Logs, screenshots, and evidence
```shell
$ pnpm openclaw --version
> [email protected] openclaw /home/orin/Gittensor/Test/openclaw
> node scripts/run-node.mjs --version
OpenClaw 2026.4.27 (bfdee7c)
$ pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping"
> node scripts/run-node.mjs cron add --name probetest --cron not-a-cron-expr --message ping
GatewayClientRequestError: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
ELIFECYCLE Command failed with exit code 1.
$ pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping"
> node scripts/run-node.mjs cron add --name probetest --cron '99 * * * *' --message ping
GatewayClientRequestError: RangeError: CronPattern: Invalid value for minute: 99
ELIFECYCLE Command failed with exit code 1.
Server-side log entries (one INFO + two ERRORs per bad input, on every attempt):
$ pnpm openclaw logs --max-bytes 60000 | grep -E "TypeError.*CronPattern|RangeError.*CronPattern|errorCode=UNAVAILABLE.*cron"
2026-04-29T03:23:31.476Z error gateway request handler failed: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:31.480Z info gateway/ws ⇄ res ✗ cron.add 22ms errorCode=UNAVAILABLE errorMessage=TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:31.496Z error GatewayClientRequestError: TypeError: CronPattern: invalid configuration format ('not-a-cron-expr'), exactly five, six, or seven space separated parts are required.
2026-04-29T03:23:37.686Z error gateway request handler failed: RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:23:37.693Z info gateway/ws ⇄ res ✗ cron.add 11ms errorCode=UNAVAILABLE errorMessage=RangeError: CronPattern: Invalid value for minute: 99
2026-04-29T03:23:37.721Z error GatewayClientRequestError: RangeError: CronPattern: Invalid value for minute: 99
Parallel clean path for comparison (--every is client-side validated, no gateway log noise, no class-name leak):
$ pnpm openclaw cron add --name probetest --every "BogusDuration" --message "ping"
Error: Invalid --every; use e.g. 10m, 1h, 1d
ELIFECYCLE Command failed with exit code 1.
Impact and severity
Affected users/systems/channels: every operator using openclaw cron add or openclaw cron edit with a --cron expression. Also affects any non-CLI client (Control UI, MCP, RPC clients) that calls cron.add/cron.update with a malformed pattern. Not platform- or channel-specific.
Severity: reliability / observability degradation. Each bad-pattern attempt adds two error gateway request handler failed: TypeError|RangeError entries to gateway.log — exactly the line shape operators search for to spot true handler crashes. In support triage, this means false positives crowd out signal. The wrong errorCode=UNAVAILABLE also misleads any client that implements retry-on-UNAVAILABLE: the client will retry a permanently malformed pattern indefinitely. The CLI-side class-name leak (GatewayClientRequestError: TypeError: ...) breaks the principle that user-facing error text should not expose JS internals.
Frequency: always. Deterministic on every malformed --cron value to cron.add or cron.update.
Consequence:
- Log signal/noise erosion in gateway.log (operators with many cron consumers see this regularly during onboarding and skill setup).
- Any RPC client implementing UNAVAILABLE-retry semantics will spin on permanent user errors.
- Inconsistent UX between
--every (clean) and --cron (raw class name) for the same conceptual user error class.
- Support burden: maintainers reading "request handler failed: TypeError" cannot distinguish real handler crashes from user input rejections without inspecting each line.
Additional information
Cross-references (related but distinct):
- src/gateway/server-methods/cron.ts:188-247 (cron.add handler) and :249-326 (cron.update handler) — handler shape + try/catch coverage.
- src/gateway/server/ws-connection/message-handler.ts:1586-1589 — the catch-all that emits
error gateway request handler failed: ... and maps to UNAVAILABLE. This is correct for true handler crashes but is the wrong destination for cron-pattern user errors that escape the handler.
- src/gateway/protocol/schema/error-codes.ts —
INVALID_REQUEST is the intended code for user input rejection; UNAVAILABLE is reserved for service-unavailable state.
- src/cli/cron-cli/schedule-options.ts:114 — the parallel
--every client-side validator with the clean Error: Invalid --every; use e.g. 10m, 1h, 1d message that establishes the UX baseline.
Suggested fix shape (anchor for reviewers, not prescribing the design):
- In src/gateway/server-methods/cron.ts, wrap the final
context.cron.add(jobCreate) (line 245) and context.cron.update(jobId, patch) (line 324) in try/catch. On error class === TypeError || RangeError, respond with INVALID_REQUEST and a normalized message ("invalid --cron: "). Re-throw any other error class so it stays in the legitimate "handler failed" log path.
- Consider adding a server-side cron-pattern validator alongside
validateScheduleTimestamp so the rejection happens at the same layer as the existing INVALID_REQUEST guards instead of mid-store-write.
- On the CLI side, mirror
--every's client-side validation: parse and reject malformed --cron patterns in src/cli/cron-cli/schedule-options.ts before sending to the gateway. Defense in depth, but does not replace the server-side fix (Control UI / MCP / RPC clients won't go through the CLI parser).
Not a duplicate of:
A regression test that asserts cron.add and cron.update respond with INVALID_REQUEST (not UNAVAILABLE) and do NOT log error gateway request handler failed for a known-bad pattern would pin the fix.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
Server-side cron pattern validation is missing in the gateway handlers for
cron.addandcron.update. When a user passes an invalid--cronexpression toopenclaw cron addoropenclaw cron edit, the croner library throws a rawTypeError(bad shape) orRangeError(out-of-range field) from insidecontext.cron.add(...)/context.cron.update(...)(after the handler's INVALID_REQUEST guards have already passed). The throw escapes the handler and is caught by the WS dispatcher's catch-all, which (a) logs it aterrorlevel asrequest handler failed: TypeError|RangeError: CronPattern: ...and (b) maps it ontoErrorCodes.UNAVAILABLE(a code that means "service unavailable, retry later") instead ofINVALID_REQUEST. Operators get false-positive ERROR-level entries that look like real gateway crashes, and the CLI displays a rawGatewayClientRequestError: TypeError: ...that leaks JS error class names. The matching--everyvalue path is validated cleanly client-side and producesError: Invalid --every; use e.g. 10m, 1h, 1d, so this is a server-side gap, not a CLI parser gap.Steps to reproduce
Have an OpenClaw gateway running locally (the default loopback gateway is fine; no live channels/providers needed):
pnpm openclaw status | grep "Gateway " | head -1→ "Gateway | local · ws://127.0.0.1:18789 ..."Reproduce both shapes of croner rejection (one per error class):
pnpm openclaw cron add --name probetest --cron "not-a-cron-expr" --message "ping"pnpm openclaw cron add --name probetest --cron "99 * * * *" --message "ping"pnpm openclaw cron add --name probetest --every 1h --message "ping" --json(capture id);pnpm openclaw cron edit <id> --cron "* * * 13 *"(invalid month)Inspect the gateway log:
pnpm openclaw logs --max-bytes 60000 | grep -E "request handler failed|errorCode=UNAVAILABLE.*cron|GatewayClientRequestError.*Cron"Compare with the parallel
--everypath which IS validated cleanly:pnpm openclaw cron add --name probetest --every "BogusDuration" --message "ping"→ "Error: Invalid --every; use e.g. 10m, 1h, 1d" (No gateway log entry, exit 1, clean.)Expected behavior
Bad
--cronsyntax should land on the same UX as bad--every:(a) Gateway should classify cron-pattern parse failures as user-input rejections, not handler crashes. The handler must return
ErrorCodes.INVALID_REQUESTwith a human-readable message such as "invalid --cron: ". Concrete grounded reference: the sibling INVALID_REQUEST guards already in the same file at src/gateway/server-methods/cron.ts:200-208 (normalizeCronJobCreate), :211-219 (validateCronAddParams), :225-230 (validateScheduleTimestamp), :234-243 (assertValidCronCreateDelivery), :253-262, :267-275, :297-303, :313-322 — they all wrap throwing validators in try/catch and respond with INVALID_REQUEST. The remaining call tocontext.cron.add(jobCreate)on line 245 andcontext.cron.update(jobId, patch)on line 324 are not wrapped, so any throw from croner escapes.(b) Gateway should log such rejections at
info(or at mostwarn) on the cron subsystem, not aterroron the gateway subsystem. The current catch-all in src/gateway/server/ws-connection/message-handler.ts:1586-1589 treats every uncaught handler throw aserror+UNAVAILABLE, which is correct for true unexpected failures but wrong for known-class user-input throws.(c) CLI should never display
GatewayClientRequestError: TypeError: .../GatewayClientRequestError: RangeError: ...— those error class names leak runtime internals. The matching--everypath produces a cleanError: Invalid --every; use e.g. 10m, 1h, 1d.--cronshould produce something equivalent, e.g.Error: Invalid --cron: <pattern>: <reason>.The
errorCodein the WS protocol response should beINVALID_REQUESTper the existing enum at src/gateway/protocol/schema/error-codes.ts:7, notUNAVAILABLE(which means "service unavailable" with retry semantics).Actual behavior
Three layered defects, all triggered by a single bad --cron value.
Defect 1 — server-side validation gap. src/gateway/server-methods/cron.ts:188-247 ("cron.add") guards normalizeCronJobCreate, validateCronAddParams, validateScheduleTimestamp, and assertValidCronCreateDelivery with try/catch + INVALID_REQUEST. But the final call
await context.cron.add(jobCreate)at line 245 is NOT wrapped. The croner library throws TypeError/RangeError during pattern parsing inside that call, and the throw escapes the handler. Same gap on line 324 forawait context.cron.update(jobId, patch)(cron.update handler).Defect 2 — wrong protocol error code. The WS dispatcher catch-all at src/gateway/server/ws-connection/message-handler.ts:1586-1589 maps every uncaught handler throw to:
logGateway.error(request handler failed: ${formatForLog(err)}); respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));So invalid user input ends up aserrorCode=UNAVAILABLE, which per src/gateway/protocol/schema/error-codes.ts is reserved for service-unavailable states with retry semantics — not user input rejection.Defect 3 — log noise + class-name leak in CLI output. The same dispatcher logs at
erroron thegatewaysubsystem with "request handler failed: TypeError|RangeError" — exactly the line shape operators scan for to spot real handler crashes. The CLI relays the raw error string back asGatewayClientRequestError: TypeError: CronPattern: ..., leaking JS class names.Live evidence (2026-04-29, on commit bfdee7c8):
Compare: parallel --every value path validated cleanly client-side; no gateway log entry produced.
Impact and severity
Affected users/systems/channels: every operator using
openclaw cron addoropenclaw cron editwith a--cronexpression. Also affects any non-CLI client (Control UI, MCP, RPC clients) that calls cron.add/cron.update with a malformed pattern. Not platform- or channel-specific.Severity: reliability / observability degradation. Each bad-pattern attempt adds two
error gateway request handler failed: TypeError|RangeErrorentries to gateway.log — exactly the line shape operators search for to spot true handler crashes. In support triage, this means false positives crowd out signal. The wrongerrorCode=UNAVAILABLEalso misleads any client that implements retry-on-UNAVAILABLE: the client will retry a permanently malformed pattern indefinitely. The CLI-side class-name leak (GatewayClientRequestError: TypeError: ...) breaks the principle that user-facing error text should not expose JS internals.Frequency: always. Deterministic on every malformed
--cronvalue to cron.add or cron.update.Consequence:
--every(clean) and--cron(raw class name) for the same conceptual user error class.Additional information
Cross-references (related but distinct):
error gateway request handler failed: ...and maps toUNAVAILABLE. This is correct for true handler crashes but is the wrong destination for cron-pattern user errors that escape the handler.INVALID_REQUESTis the intended code for user input rejection;UNAVAILABLEis reserved for service-unavailable state.--everyclient-side validator with the cleanError: Invalid --every; use e.g. 10m, 1h, 1dmessage that establishes the UX baseline.Suggested fix shape (anchor for reviewers, not prescribing the design):
context.cron.add(jobCreate)(line 245) andcontext.cron.update(jobId, patch)(line 324) in try/catch. On error class === TypeError || RangeError, respond withINVALID_REQUESTand a normalized message ("invalid --cron: "). Re-throw any other error class so it stays in the legitimate "handler failed" log path.validateScheduleTimestampso the rejection happens at the same layer as the existing INVALID_REQUEST guards instead of mid-store-write.--every's client-side validation: parse and reject malformed--cronpatterns in src/cli/cron-cli/schedule-options.ts before sending to the gateway. Defense in depth, but does not replace the server-side fix (Control UI / MCP / RPC clients won't go through the CLI parser).Not a duplicate of:
agentdispatch times out 60s + embedded mode contends with running daemon for session file locks #71605, session file locked when gateway times out and falls back to embedded runner #62981 — about gateway timeouts + embedded fallback contention (different surface).A regression test that asserts
cron.addandcron.updaterespond withINVALID_REQUEST(notUNAVAILABLE) and do NOT logerror gateway request handler failedfor a known-bad pattern would pin the fix.