Config Error Handling
Two failure phases, two reporting paths
Membrane config errors surface differently depending on when they're thrown, and that's the
first thing to get right.
Setter-time — inside an @MCAttribute/@MCChildElement setter, during YAML property binding
(annot/.../yaml/parsing/binding/PropertyBinder.java, populate()). That catch (Throwable cause)
re-wraps into new ConfigurationParsingException(cause.getMessage()) and attaches a
ParsingContext (source location), so RouterCLI renders a highlighted > YAML snippet pointing
at the offending line. Mind the message: reflection wraps a setter's own exception in
InvocationTargetException, and PropertyBinder does not unwrap it, so the message it copies is
the wrapper's (null) — the real text sits in cause.getCause().getMessage(). The
@MCElement(collapsed=true) scalar path in ObjectBinder does unwrap (e.getTargetException()).
Example: WsuTimestampInterceptor.setTtl (core/.../soap/wsse/WsuTimestampInterceptor.java)
catches DateTimeParseException from Duration.parse and rethrows a ConfigurationException
with a concrete valid-example ("PT5M").
Init-time — inside interceptor.init(Router), reached via DefaultRouter.init() →
AbstractRouter.initProxies() → proxy.init(this), i.e. after all YAML parsing has finished.
The YAML bootstrap does carry a ParsingContext into the bean registry, but nothing on this
initProxies() path passes it on, so an interceptor's init() has no source location to report —
not fixable per-interceptor without a cross-cutting change to plumb it through Router/interceptor
init.
ConfigurationException thrown here is caught by
SpringConfigurationErrorHandler.handleConfigurationException
(core/.../exceptions/SpringConfigurationErrorHandler.java:128), which prints a plain
"************** Configuration Error ***..." / "Reason: <cause message>" block — no YAML
snippet. This is the existing, accepted behavior for every interceptor's init()-time checks
(missing keystore, bad key alias, unsupported algorithm, ...), not a bug to chase per class.
Prefer failing at init()-time (config-load time) over lazily at first-request time — see
DigitalSignatureInterceptor.validateConfiguration() for the pattern (checked in init(), before
any request is handled).
Writing a good ConfigurationException message
- Name the bad attribute and the value that was given.
- List valid options built from real constants, not hand-typed strings, so the list can't drift
from what's actually supported — e.g.
DigitalSignatureInterceptor.SUPPORTED_DIGEST_ALGORITHMS
is built from javax.xml.crypto.dsig.DigestMethod.*, verified experimentally against the JDK's
DOM XMLSignatureFactory rather than assumed from the constant list.
- Wrap the causing checked exception with
new ConfigurationException(message, cause) rather than
swallowing it — cause.getMessage() is still visible via Reason: in the init-time path.
Testing pattern
- Init-time checks: follow
DigitalSignatureInterceptorTest's initWith(references...) +
assertThrows(RuntimeException.class, () -> initWith(...)) idiom — set the invalid state on a
fresh interceptor with an otherwise-valid config, assert init throws.
- Setter-time message propagation:
annot/src/test/java/.../YAMLParsingErrorTest.java builds
ad-hoc @MCElement classes and asserts on the rendered error report (see attribute()) — use
that as the template. There is currently no test covering a setter-thrown exception's message,
which is why the InvocationTargetException gap above goes unnoticed; add one there if you touch
that path.
1---2name: config-error-handling3description: Add or improve config validation and error messages for Membrane config elements (@MCElement classes) — throwing/wrapping ConfigurationException, deciding where to validate, and understanding why some config errors get a highlighted YAML snippet and others don't. Use whenever asked to add config validation, throw a ConfigurationException, make a config error message more informative, or explain why a config error shows/doesn't show the YAML `>` marker.4---56# Config Error Handling78## Two failure phases, two reporting paths910Membrane config errors surface differently depending on *when* they're thrown, and that's the11first thing to get right.1213**Setter-time** — inside an `@MCAttribute`/`@MCChildElement` setter, during YAML property binding14(`annot/.../yaml/parsing/binding/PropertyBinder.java`, `populate()`). That `catch (Throwable cause)`15re-wraps into `new ConfigurationParsingException(cause.getMessage())` and attaches a16`ParsingContext` (source location), so `RouterCLI` renders a highlighted `>` YAML snippet pointing17at the offending line. Mind the message: reflection wraps a setter's own exception in18`InvocationTargetException`, and `PropertyBinder` does *not* unwrap it, so the message it copies is19the wrapper's (`null`) — the real text sits in `cause.getCause().getMessage()`. The20`@MCElement(collapsed=true)` scalar path in `ObjectBinder` does unwrap (`e.getTargetException()`).21Example: `WsuTimestampInterceptor.setTtl` (`core/.../soap/wsse/WsuTimestampInterceptor.java`)22catches `DateTimeParseException` from `Duration.parse` and rethrows a `ConfigurationException`23with a concrete valid-example ("PT5M").2425**Init-time** — inside `interceptor.init(Router)`, reached via `DefaultRouter.init()` →26`AbstractRouter.initProxies()` → `proxy.init(this)`, i.e. *after* all YAML parsing has finished.27The YAML bootstrap does carry a `ParsingContext` into the bean registry, but nothing on this28`initProxies()` path passes it on, so an interceptor's `init()` has no source location to report —29not fixable per-interceptor without a cross-cutting change to plumb it through `Router`/interceptor30init.31`ConfigurationException` thrown here is caught by32`SpringConfigurationErrorHandler.handleConfigurationException`33(`core/.../exceptions/SpringConfigurationErrorHandler.java:128`), which prints a plain34`"************** Configuration Error ***..."` / `"Reason: <cause message>"` block — no YAML35snippet. This is the existing, accepted behavior for every interceptor's init()-time checks36(missing keystore, bad key alias, unsupported algorithm, ...), not a bug to chase per class.3738Prefer failing at init()-time (config-load time) over lazily at first-request time — see39`DigitalSignatureInterceptor.validateConfiguration()` for the pattern (checked in `init()`, before40any request is handled).4142## Writing a good ConfigurationException message4344- Name the bad attribute and the value that was given.45- List valid options built from real constants, not hand-typed strings, so the list can't drift46 from what's actually supported — e.g. `DigitalSignatureInterceptor.SUPPORTED_DIGEST_ALGORITHMS`47 is built from `javax.xml.crypto.dsig.DigestMethod.*`, verified experimentally against the JDK's48 DOM `XMLSignatureFactory` rather than assumed from the constant list.49- Wrap the causing checked exception with `new ConfigurationException(message, cause)` rather than50 swallowing it — `cause.getMessage()` is still visible via `Reason:` in the init-time path.5152## Testing pattern5354- **Init-time checks**: follow `DigitalSignatureInterceptorTest`'s `initWith(references...)` +55 `assertThrows(RuntimeException.class, () -> initWith(...))` idiom — set the invalid state on a56 fresh interceptor with an otherwise-valid config, assert init throws.57- **Setter-time message propagation**: `annot/src/test/java/.../YAMLParsingErrorTest.java` builds58 ad-hoc `@MCElement` classes and asserts on the rendered error report (see `attribute()`) — use59 that as the template. There is currently no test covering a setter-thrown exception's message,60 which is why the `InvocationTargetException` gap above goes unnoticed; add one there if you touch61 that path.