{"record":{"id":"21eed077f733a05e","repo":"junit-team/junit5","slug":"chain-of-invocationinterceptors-never-called-invoc","errorCode":null,"errorMessage":"Chain of InvocationInterceptors never called invocation: %s","messagePattern":"Chain of InvocationInterceptors never called invocation: (.+?)","errorType":"exception","errorClass":"JUnitException","httpStatus":null,"severity":"error","filePath":"junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/InvocationInterceptorChain.java","lineNumber":149,"sourceCode":"\t\t\tdelegate.skip();\n\t\t}\n\n\t\tprivate void markInvokedOrSkipped() {\n\t\t\tif (!invokedOrSkipped.compareAndSet(false, true)) {\n\t\t\t\tfail(\"Chain of InvocationInterceptors called invocation multiple times instead of just once\");\n\t\t\t}\n\t\t}\n\n\t\tvoid verifyInvokedAtLeastOnce() {\n\t\t\tif (!invokedOrSkipped.get()) {\n\t\t\t\tfail(\"Chain of InvocationInterceptors never called invocation\");\n\t\t\t}\n\t\t}\n\n\t\tprivate void fail(String prefix) {\n\t\t\tString commaSeparatedInterceptorClasses = interceptors.stream().map(Object::getClass).map(\n\t\t\t\tClass::getName).collect(joining(\", \"));\n\t\t\tthrow new JUnitException(prefix + \": \" + commaSeparatedInterceptorClasses);\n\t\t}\n\t}\n\n}\n","sourceCodeStart":131,"sourceCodeEnd":154,"githubUrl":"https://github.com/junit-team/junit5/blob/f070c699a08b5d8393df9afd147af5c5e90bb21b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/InvocationInterceptorChain.java#L131-L154","documentation":"Internal assertion inside ValidatingInvocation: after running the entire InvocationInterceptor chain for an intercepted call (constructor/method/lifecycle), invokedOrSkipped was never set, meaning no interceptor ever called Invocation.proceed() or Invocation.skip(). The message lists every interceptor in the chain so you can see which one swallowed the call.","triggerScenarios":"An InvocationInterceptor.intercept* method returns without ever calling invocation.proceed() or invocation.skip(); verifyInvokedAtLeastOnce() then fires at the end of InvocationInterceptorChain.invoke. The throw site is at line 142-143 / fail() at 149.","commonSituations":"A third-party or custom InvocationInterceptor short-circuits execution intentionally but forgets to either proceed or skip; an interceptor swallows the Invocation and substitutes its own logic without delegating; an interceptor throws but the throw is caught upstream leaving the chain un-proceeded (rare).","solutions":["Identify the listed interceptor that owns the missing proceed() call.","Ensure the interceptor always either calls invocation.proceed() (to continue) or invocation.skip() (to halt) on every code path.","If the interceptor intentionally replaces the invocation, call proceed() against the wrapped chain or invoke the underlying target directly and return its value - but never silently drop."],"exampleFix":"// before\n@Override\npublic <T> T interceptTestClassConstructor(Invocation<T> inv, ReflectiveInvocationContext<T> ctx, ExtensionContext ec) {\n    log(\"creating\");\n    return null; // forgot proceed()\n}\n// after\n@Override\npublic <T> T interceptTestClassConstructor(Invocation<T> inv, ReflectiveInvocationContext<T> ctx, ExtensionContext ec) throws Throwable {\n    log(\"creating\");\n    return inv.proceed();\n}","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Every InvocationInterceptor.intercept* overload must call invocation.proceed() or invocation.skip() on every path.","Audit try/catch/finally blocks in interceptors to ensure exactly one proceed() invocation.","Write a unit test that wraps your interceptor in an InvocationInterceptorChain and asserts invocation completes."],"tags":["invocation-interceptor","internal-assertion","extension","chain-violation"],"backgroundTag":null,"analyzedSha":"f070c699a08b5d8393df9afd147af5c5e90bb21b","analyzedAt":"2026-08-11T20:31:00.530Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}