Serdar Gökay created CAMEL-24320:
------------------------------------
Summary: camel-core - Kamelet route creation fails with virtual
threads enabled on JDK 25
Key: CAMEL-24320
URL: https://issues.apache.org/jira/browse/CAMEL-24320
Project: Camel
Issue Type: Bug
Components: camel-core, camel-kamelet
Affects Versions: 4.21.0
Environment: Apache Camel Main 4.21.0; Eclipse Temurin 25.0.3;
camel.threads.virtual.enabled=true; reproduced in Docker. Control: the
identical route starts and consumes timer ticks with virtual threads disabled.
Reporter: Serdar Gökay
Attachments: camel-kamelet-vt-jdk25-reproducer.zip
h3. What happens
With {{-Dcamel.threads.virtual.enabled=true}} on JDK 25, a route that
consumes a Kamelet fails during route creation with a
{{NullPointerException}}. The same route starts and runs normally with
virtual threads disabled.
h3. Root cause
# On JDK 25 with virtual threads enabled, the multi-release
{{ContextValueFactory}} ({{core/camel-util}}, Java 25 variant) selects the
{{ScopedValue}}-backed {{ScopedValueContextValue}} because
{{ThreadType.current()}} reports {{VIRTUAL}}.
# {{DefaultCamelContextExtension.getCreateRoute()}} and
{{getCreateProcessor()}} are implemented as {{isCreateRoute.orElse(null)}}
and {{isCreateProcessor.orElse(null)}}
({{DefaultCamelContextExtension.java:330,335}}).
# {{ScopedValueContextValue.orElse}} ({{ContextValueFactory.java:141}})
delegates to {{java.lang.ScopedValue.orElse}}, which on JDK 25 executes
{{Objects.requireNonNull(other)}} before any binding lookup — a null
fallback therefore throws unconditionally, even where a binding exists.
# {{KameletComponent}} reads {{getCreateRoute()}} while initializing its
endpoint service ({{KameletComponent.java:184}}), outside any binding scope,
so every Kamelet route creation throws NPE before the route exists.
There is no configuration that keeps virtual threads enabled while forcing
the {{ThreadLocal}}-backed {{ContextValue}}; the implementation is selected
solely from {{ThreadType.current()}}. The defect is still present on {{main}}
as of 2026-07-31 (call sites and implementation unchanged).
h3. Stack trace (camel 4.21.0, Temurin 25.0.3)
{code}
Exception in thread "main" org.apache.camel.FailedToCreateRouteException:
Failed to create route: vt-kamelet-repro (source: file:route.camel.yaml):
Route(vt-kamelet-repro)[From[kamelet:vt-repro-source] -> [Lo... because: Failed
to resolve endpoint: kamelet://vt-repro-source due to: null
at
org.apache.camel.reifier.RouteReifier.createRoute(RouteReifier.java:96)
...
Caused by: org.apache.camel.ResolveEndpointFailedException: Failed to resolve
endpoint: kamelet://vt-repro-source due to: null
at
org.apache.camel.impl.engine.AbstractCamelContext.doGetEndpoint(AbstractCamelContext.java:891)
...
Caused by: java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Unknown Source)
at java.base/java.lang.ScopedValue.orElse(Unknown Source)
at
org.apache.camel.util.concurrent.ContextValueFactory$ScopedValueContextValue.orElse(ContextValueFactory.java:141)
at
org.apache.camel.impl.engine.DefaultCamelContextExtension.getCreateRoute(DefaultCamelContextExtension.java:330)
at
org.apache.camel.component.kamelet.KameletComponent$1.doInit(KameletComponent.java:184)
{code}
h3. Reproducer (plain Camel Main, two small files)
{{route.camel.yaml}}:
{code}
- route:
id: vt-kamelet-repro
from:
uri: kamelet:vt-repro-source
steps:
- log: "consumed ${body}"
{code}
{{kamelets/vt-repro-source.kamelet.yaml}}:
{code}
apiVersion: camel.apache.org/v1
kind: Kamelet
metadata:
name: vt-repro-source
labels:
camel.apache.org/kamelet.type: source
spec:
definition:
title: VT repro source
template:
from:
uri: timer:tick
parameters:
period: "1000"
steps:
- setBody:
constant: hello
- to:
uri: kamelet:sink
{code}
Run with any JDK 25 and camel-main 4.21.0 (kamelets/ on the classpath):
{code}
java -cp <camel-main-4.21.0-classpath>:. \
-Dcamel.threads.virtual.enabled=true \
-Dcamel.main.routesIncludePattern=file:route.camel.yaml \
org.apache.camel.main.Main
{code}
→ {{FailedToCreateRouteException}} with the NPE above at startup.
With {{-Dcamel.threads.virtual.enabled=false}} the identical files start and
consume timer ticks.
h3. Suggested fix direction
Make the {{ScopedValue}}-backed {{orElse}} null-tolerant — e.g. return the
default when unbound instead of delegating a null fallback
({{scopedValue.isBound() ? scopedValue.get() : defaultValue}}) — or change
the two call sites to a non-null fallback. A regression test that creates a
Kamelet route with virtual threads enabled on JDK 25 would cover the path;
note that {{getCreateProcessor()}} carries the same defect for any component
reading it during endpoint creation.
Related: CAMEL-20199 (virtual threads umbrella).
--
This message was sent by Atlassian Jira
(v8.20.10#820010)