flowable/flowable-engine · error · PropertyNotWritableException

resolver is read-only

Error message

resolver is read-only

What it means

BeanELResolver.setValue throws PropertyNotWritableException("resolver is read-only") when a value is assigned through an EL expression on a resolver that was constructed with readOnly=true. The resolver refuses all writes regardless of the target bean.

Source

Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/javax/el/BeanELResolver.java:172

			return invoke(m, base, (Object[]) null);
		} catch (InvocationTargetException e) {
			Throwable cause = e.getCause();
			Util.handleThrowable(cause);
            throw new ELException("Error reading '" + property + "' on type '" + base.getClass().getName() + "'", cause);
		} catch (Exception e) {
			throw new ELException(e);
		}
	}

	@Override
	public void setValue(ELContext context, Object base, Object property, Object value) {
		Objects.requireNonNull(context, "context is null");
		if (base == null || property == null) {
			return;
		}

		if (readOnly) {
			throw new PropertyNotWritableException("resolver is read-only");
		}
		BeanProperty beanProperty = property(base, property);

		if (beanProperty == null) {
			return;
		}

		Method method = beanProperty.write(base);
		if (method == null) {
			throw new PropertyNotWritableException("Cannot write property: '" + property + "' on type " + base.getClass().getName());
		}
		context.setPropertyResolved(base, property);
		try {
			invoke(method, base, value);
		} catch (InvocationTargetException e) {
			Throwable cause = e.getCause();
			Util.handleThrowable(cause);
			throw new ELException("Error '" + property + "' on type '" + base.getClass().getName() + "'", cause);

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Remove the assignment from the expression; use reads only.
  2. If writes are legitimately needed, construct the resolver with readOnly=false.
  3. Perform the mutation in code (bean setter) rather than inside the EL expression.

Example fix

// before
ELResolver r = new BeanELResolver(true); // read-only
// after
ELResolver r = new BeanELResolver(false); // only if writes are intended
Defensive patterns

Strategy: validation

Validate before calling

if (needsWrites) resolver = new BeanELResolver(false); // else keep readOnly=true and never emit assignment expressions

Try / catch

try { resolver.setValue(ctx, base, prop, val); } catch (PropertyNotWritableException e) { /* route write elsewhere or rebuild resolver writable */ }

Prevention

When it happens

Trigger: Evaluating an assignment expression like ${bean.name = 'x'} (or calling setValue directly) against a BeanELResolver created as new BeanELResolver(true) — e.g. a shared read-only resolver used in Flowable's EL context.

Common situations: Reusing a read-only resolver configured for safe expression evaluation in a context that also attempts writes; accidental assignment syntax in templates meant to be read-only.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/92baddd7a971ab53. Report an issue: GitHub.