Fail closed
A rule that cannot be evaluated never passes silently. It blocks, and the engine says so.
Sometimes a rule cannot be evaluated: it divides by zero, compares a string with gt, or calls a
custom operator the host did not register. The engine never treats that as a pass. The rule
produces a finding with the code RULE-EVALUATION-ERROR, severity error, blocking: true, and
the message "This rule could not be evaluated." The decision is deny.
An error in an action rule also withdraws every command of the request. Nothing is announced
for a change that will not be saved.
Syntax
- rule: <rule id>
code: RULE-EVALUATION-ERROR
severity: error
blocking: true
message: This rule could not be evaluated.Example
The command's idempotency key divides the total by the quantity. With 0 items the action rule fails, so the request is denied and no command comes back.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.fail-closed, version: 1.0.0, title: Fail closed }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.total.positive
kind: validation
target: { entity: Order, field: /total }
operations: [create]
assert: { op: gt, args: [{ var: data.total }, 0] }
severity: error
finding: { code: LRN-FCL-001, message: order.totalNotPositive }
- id: order.created.event
kind: action
target: { entity: Order }
operations: [create]
enforcement: server
commands:
- name: order.created
type: event
idempotencyKey: [{ var: data.id }, { op: div, args: [{ var: data.total }, { var: data.quantity }] }]
messages:
en:
order.totalNotPositive: "The total must be more than 0."
tests:
- name: an action that divides by zero denies and withdraws every command
entity: Order
operation: create
given:
data: { id: "o-1", quantity: 0, total: 20 }
expect:
decision: deny
findings:
- { rule: order.created.event, code: RULE-EVALUATION-ERROR, severity: error, blocking: true, message: This rule could not be evaluated. }
commands: []
- name: a text total is a type error, never a silent pass
entity: Order
operation: create
given:
data: { id: "o-2", quantity: 2, total: "20" }
expect:
decision: deny
findings:
- { rule: order.total.positive, code: RULE-EVALUATION-ERROR }
commands: []
- name: a normal order is allowed and announced
entity: Order
operation: create
given:
data: { id: "o-3", quantity: 4, total: 20 }
expect:
decision: allow
findings: []
commands: [order.created]{
"entity": "Order",
"operation": "create",
"data": {
"id": "o-1",
"quantity": 0,
"total": 20
}
}Result, from the engine
Decisiondeny1 finding, server channel
RULE-EVALUATION-ERRORerrorblockingThis rule could not be evaluated.(no field)
The second golden test sends "20" (a string) as the total. gt takes two numbers and there is no
implicit coercion, so that rule fails closed too.
Common mistakes
- Ignoring the code.
RULE-EVALUATION-ERRORin production means a rule meets data it was not written for. Alert on it, and fix the rule or the caller. - Guarding too late. Use
whento skip a rule that cannot apply, such as a division when the divisor is 0. A skipped rule produces nothing. - Hoping for a default. A missing value is
null, andnullis not a number. Check it withexists, or give it a value withcoalesce.
Exercise
Make the action rule skip orders without items. An order of 0 items must then be allowed with no
command, and an order of 4 items still announced with order.created.
Hint
Add a when to the action rule: quantity greater than 0.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.fail-closed, version: 1.0.0, title: Fail closed }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.total.positive
kind: validation
target: { entity: Order, field: /total }
operations: [create]
assert: { op: gt, args: [{ var: data.total }, 0] }
severity: error
finding: { code: LRN-FCL-001, message: order.totalNotPositive }
- id: order.created.event
kind: action
target: { entity: Order }
operations: [create]
enforcement: server
when: { op: gt, args: [{ var: data.quantity }, 0] }
commands:
- name: order.created
type: event
idempotencyKey: [{ var: data.id }, { op: div, args: [{ var: data.total }, { var: data.quantity }] }]
messages:
en:
order.totalNotPositive: "The total must be more than 0."
tests:
- name: an order without items is allowed and not announced
entity: Order
operation: create
given:
data: { id: "o-1", quantity: 0, total: 20 }
expect:
decision: allow
findings: []
commands: []
- name: a normal order is still announced
entity: Order
operation: create
given:
data: { id: "o-3", quantity: 4, total: 20 }
expect:
decision: allow
findings: []
commands: [order.created]{
"entity": "Order",
"operation": "create",
"data": {
"id": "o-1",
"quantity": 0,
"total": 20
}
}Result, from the engine
Decisionallow0 findings, server channel