Apache Camel 4.x Upgrade Guide
This document is for helping you upgrade your Apache Camel application from Camel 4.x to 4.y. For example, if you are upgrading Camel 4.0 to 4.2, then you should follow the guides from both 4.0 to 4.1 and 4.1 to 4.2.
| The Camel Upgrade Recipes project provides automated assistance for some common migration tasks. Note that manual migration is still required. See the documentation page for details. |
Upgrading Camel 4.23 to 4.24
camel-semantic - expert artifacts
TypeSafe AI Semantic applications must add org.apache.camel:camel-semantic-typesafe-ai. The camel-typesafe-ai component and TypeSafe AI language remain independently usable and no longer bring in camel-semantic. The new expert artifact includes both dependencies transitively. Existing bean declarations and imports for org.apache.camel.component.typesafeai.TypeSafeAiSemanticAdapter remain valid.
Spring Boot applications using this expert must keep camel-typesafe-ai-starter and add org.apache.camel:camel-semantic-typesafe-ai at the matching Camel version. The existing starter continues configuring the component and TypeSafe AI language; register the Semantic expert explicitly as a named Spring bean. Applications using only the component or TypeSafe AI language need no new dependency.
Replace org.apache.camel:camel-wolf-defender with org.apache.camel:camel-semantic-wolf-defender. The Wolf Defender Java packages, classes, model provisioning and configuration properties are unchanged. Its documentation page is now semantic-wolf-defender.adoc.
Update Maven dependencies and Camel JBang --dep options to the new expert artifact names. Each Semantic evaluation still selects an explicit named registry expert. Provider identities and operation names remain typesafe-ai and wolf-defender; only the reported adapter artifact changes.
TypeSafe AI now rejects explicit media parts nested in structured input, instructions or criteria, which previously could be serialized and sent to the provider. Text and JSON-compatible Map/List state remain supported. Binary objects and image, audio or video parts are rejected before HTTP; plain text and ordinary application field names do not imply media. Records such as {type: image, url: …, sku: 42} and MIME metadata with a URL alone remain application state. Direct Semantic adapter calls and batches use the same validation. Wolf Defender remains text-only with its existing limits.
camel-semantic - explicit experts
Every evaluation in the preview semantic API now requires an explicit expert registry bean name. Move block-level expert values onto each evaluation in YAML and XML; in Java, move .expert(…) from SemanticEvaluationsBuilder to each SemanticEvaluationBuilder.
The camel.language.semantic.default-expert and camel.language.semantic.adapter options, SemanticLanguage.setDefaultExpert / setAdapter, automatic selection of a sole expert, and META-INF/services/org/apache/camel/semantic-adapter discovery are removed. Register an expert bean explicitly, including its initialization and destruction methods where needed, and reference it from each evaluation. Expert references accept property placeholders, but do not fall back to class loading. The default state selector is unchanged.
The semantic metadata response no longer includes defaultExpert, defaultError or defaultErrorCode. Operation inspection and completion require an explicit expert reference.
camel-semantic - evaluation-only auditing
The preview semantic audit API no longer records route actions. SemanticAuditDecision, SemanticAudit.get(context).decision(…) and SemanticObserver.decision(…) have been removed. Remove the corresponding decision beans and processor calls from routes. Evaluation auditing is automatic under the existing audit configuration; normal application routing remains unchanged. Previously persisted version-1 decision records remain readable and exportable as legacy data. Semantic audit browsing no longer accepts route-action, namespace or application-correlation filters. The console’s target filter is also removed. Newly captured evaluation records use definition instead of target; update consumers and queries to read definition. Previously persisted records retain their original fields. Event-ID detail responses no longer expand a decision’s evidence references automatically; use caseEventId for a bounded associated case or eventIds for explicit bulk retrieval. The original persisted record remains available in record. Use automatic evaluation provenance, including entry/executing route and Camel breadcrumb identifiers. The internal CamelSemanticAuditReferences exchange property is no longer populated. In audit health responses, capturing replaces decisionsEnabled; it indicates that some expert enables evaluation capture. For external readers, retained is unknown instead of a count from the unrelated memory store.
SemanticAuditReader.getAll no longer imposes a 100-ID evidence limit. Callers must bound requests and responses; backend implementations may split retrieval into their own batches. The 100-reference limit on legacy persisted evidence records remains unchanged.
Vert.x 5
Camel now uses Vert.x 5. This affects camel-vertx, camel-vertx-http, camel-vertx-websocket, camel-platform-http-vertx, camel-platform-http-main, camel-platform-http-jolokia, camel-knative-http and camel-oauth. Application code that uses the Vert.x API directly, for example to create a custom Vertx instance, a custom Router handler, or an authentication provider for platform-http, must be migrated as described in the Vert.x 5 migration guide. Notably, the callback variants of asynchronous methods are removed in favour of the methods returning a Future.
A metrics or tracing factory can no longer be set on the VertxOptions given to a component (the MetricsOptions.setFactory and TracingOptions.setFactory methods are removed). Either create the Vertx instance with Vertx.builder() and set it on the component (or bind it to the registry), or register the factory as a io.vertx.core.spi.VertxServiceProvider service.
VertxPlatformHttpRouter.mountSubRouter is removed, as it is no longer part of the Vert.x Router API. Use router.route(mountPoint + "*").subRouter(subRouter) instead.
When the connection of a WebSocket is lost without a close frame, the camel-vertx-websocket consumer no longer passes the resulting exception to the consumer exception handler, and no longer routes it as an ERROR event. It is handled as a WebSocket closed with a close frame: a consumer with fireWebSocketConnectionEvents=true routes a CLOSE event, and a consumer with consumeAsClient=true reconnects. With the default fireWebSocketConnectionEvents=false, the lost connection of a peer is therefore no longer logged as a warning.
When the WebSocket of a consumer with consumeAsClient=true reports any other error, the consumer exception handler now receives that exception. Previously it received the cause of that exception, which could be null.
When the WebSocket that the camel-vertx-websocket producer sends to (a server not managed by Camel) reports an error other than a lost connection, the exchange now fails with that exception. Previously it failed with the cause of that exception, which could be null.
camel-arangodb still uses Vert.x 4, as the ArangoDB Java driver does not support Vert.x 5 yet (see arangodb-java-driver#625). An application that uses camel-arangodb together with one of the components above resolves a single version of the io.vertx artifacts, so either the ArangoDB driver or the other components fail at runtime. The workaround suggested by the driver maintainers is to replace com.arangodb:arangodb-java-driver with com.arangodb:arangodb-java-driver-shaded, which embeds its own relocated copy of Vert.x. The vertx option of camel-arangodb cannot be used in that case.
camel-kubernetes - Kubernetes client 8
camel-kubernetes now uses the fabric8 Kubernetes client 8.0, which brings these changes for applications:
-
The client uses its Vert.x 5 HTTP client (
io.fabric8:kubernetes-httpclient-vertx-5) instead of the Vert.x 4 one (io.fabric8:kubernetes-httpclient-vertx). An application that excludes the default HTTP client, for example to use OkHttp or the JDK HTTP client, must now excludekubernetes-httpclient-vertx-5. -
The client and its model use Jackson 3 (
tools.jackson). Jackson 2 does not apply the serializers of the model, so Kubernetes model objects serialized with Jackson 2 come out in a different shape. For example, atargetPortof8080is written as{"value":8080,"intVal":8080}, and a CPU limit of500mas{"amount":"500","format":"m"}. This affects routes that marshal the result of acamel-kubernetesproducer with thecamel-jacksondata format (marshal().json()), and application code that uses its own Jackson 2ObjectMapper. Replace thecamel-jacksondependency withcamel-jackson3, which provides the samejacksondata format on Jackson 3 (see JSON Jackson 3), so the route does not change. In Java code, use Jackson 3 orio.fabric8.kubernetes.client.utils.Serialization. -
Jackson 3 uses the Jackson 2
com.fasterxml.jackson.core:jackson-annotationsartifact, and needs version 2.21 or later. An application that pins Jackson 2 to 2.20 or older, for example through an older Spring Boot BOM, fails on the first use of the client withNoClassDefFoundError: com/fasterxml/jackson/annotation/JsonSerializeAs. Usejackson-annotations2.21 or later; Camel uses 2.22.
See the Kubernetes client 8 migration guide for the other changes.
camel-jbang - camel validate and camel overview report the same endpoints no route consumes
camel validate source and camel overview now use the same rules for a direct:, seda: or disruptor: endpoint that a route sends to and no route consumes, so they report the same endpoints on the same project:
-
disruptor:endpoints are checked too, andcamel overviewno longer treatsdisruptor:qasseda:q(they are different endpoints at runtime);vm:anddirect-vm:, which do not exist in Camel 4, are no longer joined withseda:anddirect:. -
camel validatecounts apollorpollEnrichas consuming its endpoint, and atoDwithout an expression, anenrichwith a constant endpoint, a rest verb, and the dead letter channel of an error handler as sending to it. It reads the flows of anIntegrationresource, and stays quiet for aPipe. -
camel overviewstays quiet, ascamel validatedoes, when a route consumes an endpoint only known at runtime (from: direct:{{name}}), and no longer reports atowhose uri holds an expression.
So camel validate can report an endpoint it did not report before.
camel-jbang - camel validate normalize writes one file per input file to an output directory
camel validate normalize <files> --output=<dir> now writes one normalized file per input file into the directory, with the name of the input file, instead of merging all the routes into normalized.yaml. A top-level onException of a file then keeps applying to the routes of that file only, as it does in the source. This applies to a single input file too: camel validate normalize orders.yaml --output=out/ now writes out/orders.yaml instead of out/normalized.yaml. normalized.yaml is only written for what was not loaded from one of the input files. Input files with the same name in different directories are written as <parent directory>-<name>. Scripts that read <dir>/normalized.yaml must read the per-file outputs instead.
The command now refuses to write over one of its input files (for example --output=. in the directory of the routes) and exits with an error.
With --output=<file> or without --output, the routes are still merged, and the command now warns, on the standard error, when the top-level onException of one file applies to the routes of the other files in the merged output.
camel-jbang - camel ask asks before a call that changes something, reaches out or runs code
camel ask now asks on the terminal before a file write (showing the diff), a message to an endpoint the integration does not have, SQL that changes data, an expression in a language that runs code, stopping or restarting the integration, and a camel command that changes something. Without a terminal to ask on, as in a script, such a call does not run: pass --yes to let it run without asking.
The model gets the shared file tools (camel_get_files, camel_edit_file, camel_write_file), which validate what they write, instead of the read_file, list_files and write_file tools of camel ask. A local model gets a core set of tools (--tools=auto, the default), a hosted model the full set. The default of --max-iterations is now 25 instead of 10, and --model llama3.2 now selects that model also when a model better at tool calling, such as qwen3.6, is installed.
camel-jbang - camel get endpoint shows the scope of an endpoint
camel get endpoint replaces the REMOTE column with SCOPE, which tells three kinds of endpoint apart: remote (an external system, as before), internal (Camel plumbing that stays inside the integration, such as direct, seda or log, as the catalog marks its component internal) and local (neither, such as exec or bean). Scripts that read the REMOTE column of the table must read SCOPE instead. The --json output keeps remote and adds internal.
camel-jpa, camel-hazelcast - an exchange marked rollback only is not consumed
When a route marks the exchange rollback only (for example with markRollbackOnly() or markRollbackOnlyLast()) there is no exception on the exchange, and these consumers treated it as a success. The JPA consumer deleted the entity (or called its @Consumed method) and committed the poll, and the transacted (transacted=true) Hazelcast SEDA consumer committed its transaction, so the message was removed from the queue. Now a rollback only exchange is not consumed: the JPA consumer leaves the entity in the table, so the next poll consumes it again, as long as it still matches the query. With transacted=false the other entities of the poll are still consumed (without logging a WARN), and so is what a @PreConsumed method changed before routing; with transacted=true the whole poll is rolled back, as for a failed exchange. The Hazelcast SEDA consumer rolls back its transaction, so the message stays in the queue.
A message that the route marks rollback only every time was removed before, and is now consumed again at every poll (JPA), or taken from the queue again at once, without a delay (transacted Hazelcast SEDA, as for a failed exchange). Handle such a message in the route instead, for example by moving it aside, rather than marking it rollback only every time.
camel-sql, camel-mybatis, camel-jooq - an exchange marked rollback only last is not consumed either
The SQL, MyBatis and jOOQ consumers already left a message unconsumed when the route marked the exchange rollback only with markRollbackOnly(). They now leave it unconsumed when the route marks it rollback only last with markRollbackOnlyLast() too, as the JPA and Hazelcast SEDA consumers do: the SQL consumer runs its onConsumeFailed statement instead of onConsume (and no statement when onConsumeFailed is not set), the MyBatis consumer does not run its onConsume statement, and the jOOQ consumer does not delete the entity, so the next poll reads the row again. With transacted=true the SQL and MyBatis consumers break out of the batch and roll back. markRollbackOnlyLast() reaches the consumer only when no transacted() EIP has consumed the flag, so the consumer’s own transaction is the last one.
camel-telegram - webhook requests are verified with a secret token
In webhook mode, Camel now registers the webhook with a secret token, which Telegram sends in the X-Telegram-Bot-Api-Secret-Token header of every webhook request, and rejects a webhook request that does not carry the token with HTTP 403. The token is the value of the new webhookSecretToken option or, when that option is not set, a value derived from the authorization token. A client that calls the webhook endpoint directly, for example in a test, must send the header too.
When Camel registers the webhook at startup (webhookAutoRegister=true, the default), nothing needs to change: each upgraded instance registers the webhook with the token, and the instances that are not upgraded yet ignore the header.
When the webhook is registered in another way, through the WebhookRoutePolicy or outside Camel, the registration made with the previous version has no secret token, and the upgraded instances reject every update until the webhook is registered again with it. Register the webhook again first, then upgrade the instances that receive the updates: instances of the previous version ignore the header. With the WebhookRoutePolicy, run the registration with the new version. Outside Camel, set webhookSecretToken, and pass the same value as the secret_token parameter of setWebhook.
The default webhook path is a hash of the endpoint URI, so adding an option to the URI, such as webhookSecretToken, changes the path. When the webhook is not registered by Camel at startup, set webhookPath to keep the path that is registered.
To restore the previous behaviour, set webhookSecretTokenEnabled=false: the webhook is then registered without a secret token, and webhook requests are not verified.
camel-hashicorp-vault - properties without engine use the secret engine
A hashicorp: property without an engine, such as {{hashicorp:mysecret}} or {{hashicorp:mysecret#field}}, now reads the secret from the secret engine. Before, it read from the engine of the hashicorp: property resolved before it (or from a null engine), and {{hashicorp:mysecret#field}} always resolved to no value. Properties with an engine, such as {{hashicorp:secret:mysecret#field}}, are not affected.
A hashicorp: property without an engine and with a version, such as {{hashicorp:mysecret@2}}, now reads version 2 of the secret mysecret in the secret engine. Before, the @2 was kept as part of the secret name.
A hashicorp: property with a field of a secret that does not exist, such as {{hashicorp:secret:missing#field}}, now resolves to no value, as it already did without a field, so its default value or an optional placeholder ({{?hashicorp:secret:missing#field}}) applies. Before, without a default value, it failed with a NullPointerException.
camel-ical - each unmarshal parses with its own calendar builder
The ical data format used one ical4j CalendarBuilder for all exchanges. Two exchanges that were unmarshalled at the same time could get the events of each other, or the calendar of the other exchange. Each unmarshal now uses its own builder.
A builder also has its own time zone registry, so a VTIMEZONE of one calendar is no longer used for the calendars unmarshalled after it. A date with a TZID that is not a standard time zone and that its calendar does not define is now read as a local date-time without a zone (getDate() returns a LocalDateTime); before, it was read with the definition of an earlier message, if there was one. A calendar that defines a TZID an earlier message has defined differently is now read with its own definition.
camel-lra - only an LRA hosted by the configured coordinator is joined
The LRA saga service now joins, completes or compensates an LRA only when its URL is under the configured LRA coordinator (coordinatorUrl followed by coordinatorContextPath). A message whose Long-Running-Action header holds the URL of another coordinator now fails, instead of the saga service calling that URL.
If your participants reach the same coordinator under different URLs, for example with different host names, list the other coordinator URLs, including the context path, in the new allowedCoordinatorUrls option (camel.lra.allowedCoordinatorUrls).
This also applies to the LRAs the service starts itself, which a nested saga route looks up again: when the coordinator returns LRA URLs under another base URL than coordinatorUrl, for example behind a proxy, add that base URL to allowedCoordinatorUrls as well.
camel-lra - warning for unsigned participant callbacks, and reserved saga option names
The LRA saga service now logs a warning at startup when no callbackSecret is configured: the callbacks that the LRA coordinator sends to the participant endpoints are then not verified, so these endpoints must only be reachable by the coordinator. The Saga EIP documentation describes how to sign them.
A saga option named like a parameter of the participant callback URLs, in any case (Camel-Saga-Compensate, Camel-Saga-Complete, Camel-Saga-Signature or Camel-Saga-Lra), now fails the creation of its route, with or without a callback secret. The value of such an option could not be read back from the callback before.
camel-openai, camel-ai-tool - AI events now use CamelEvent.Type.Ai
The events fired by the AI components now share a common contract, org.apache.camel.ai.AiEvent (a CamelEvent of the new CamelEvent.Type.Ai), instead of each being a component-specific CamelEvent.Type.Custom event. Events that relate to a single exchange implement the exchange-backed subtype AiEvent.ExchangeAiEvent (the root AiEvent itself is not an ExchangeEvent). This lets an EventNotifier recognise AI activity — and route it to metrics or tracing — by type and by the AiEvent sub-interfaces, without depending on an AI component.
The events shipped in 4.23.0 are affected:
-
camel-ai-tool:AiToolAuthorizationDeniedEventnow implementsAiEvent.ToolCallDeniedEvent; -
camel-openai:OpenAIAgenticLoopStartedEvent,OpenAIAgenticToolCallExecutedEventandOpenAIAgenticLoopCompletedEventnow implementAiEvent.AgentLoopStartedEvent,AiEvent.ToolCallExecutedEventandAiEvent.AgentLoopCompletedEvent.
Their getType() now returns CamelEvent.Type.Ai rather than CamelEvent.Type.Custom, and they are now CamelEvent.ExchangeEvent`s (via `AiEvent.ExchangeAiEvent). An EventNotifier that recognised these events by event.getType() == CamelEvent.Type.Custom must match on CamelEvent.Type.Ai instead, or — preferably — on event instanceof org.apache.camel.ai.AiEvent (or one of its sub-interfaces), which no longer requires a dependency on the AI component. The concrete event classes and all their accessors are unchanged, so code that references them directly is not affected.
Because the shipped AI events are now ExchangeEvent`s (via `AiEvent.ExchangeAiEvent), a notifier that filters on instanceof ExchangeEvent now also receives them. AI events are their own category: a new ignoreAiEvents flag (EventNotifier.setIgnoreAiEvents(boolean), default false) opts a notifier out, and ignoreExchangeEvents does not drop AI events (so an AI-observability notifier can keep ignoreExchangeEvents=true for performance). Camel’s own exchange-oriented notifiers (the debugger, the dev-console event ring, the micrometer and OpenTelemetry metrics exchange notifiers) set ignoreAiEvents=true so AI events do not reach them; a custom exchange-focused notifier that should not see AI events should do the same.
camel-oauth
OAuthBearerTokenProcessor no longer creates or updates the OAuth session, and no longer sets the camel.oauth.session cookie nor the CamelOAuthSessionId header: a bearer token authenticates the request that carries it, and nothing else. The authenticated UserProfile is available in the CamelOAuthUserProfile exchange property (OAuth.CAMEL_OAUTH_USER_PROFILE). A route that read the profile from the OAuth session after OAuthBearerTokenProcessor must read it from that exchange property instead.
When the message comes from platform-http (on Vert.x or on Spring Boot), from a servlet based HTTP consumer (camel-servlet, camel-jetty) or from camel-netty-http, the bearer token and the session cookie are now read from the Authorization and Cookie headers of the HTTP request only. A query or form parameter of the same name, which these consumers also map into the message headers, is no longer used for them, and neither is an Authorization or Cookie message header that the route sets itself. For example .setHeader("Authorization", simple("Bearer ${header.X-Forwarded-Access-Token}")) before OAuthBearerTokenProcessor now results in a 401 response. A route that takes the token from elsewhere can validate it with OAuthHelper.validateOAuthToken from camel-support instead.
For any other message, such as one from camel-undertow, the processors keep reading the message headers, but leave out a value that also appears in the query string of the request.
camel-platform-http-vertx - the charset parameter of the request Content-Type in any case
The consumer now reads the charset parameter of the request Content-Type whatever the case of its name, as camel-undertow and camel-netty-http do: a body sent with Content-Type: text/plain; Charset=ISO-8859-1 is decoded as ISO-8859-1 and CamelCharsetName is set. Before, only charset= in lower case was read and such a body was decoded as UTF-8. An empty charset parameter (charset= or charset="") now counts as no charset, as in camel-undertow, camel-netty-http and camel-http-common (CAMEL-25355); before, converting the body to a String failed with 500.
camel-report-maven-plugin - Java routes are read with the Java DSL parser
The validate and route-coverage goals read Java routes with the Java DSL parser of camel-java-io instead of camel-route-parser (Roaster). XML routes are read as before. The parser reads every configure() method of a source file, including those of a route builder created inside a test, so validate can find and validate endpoints it did not see before. An endpoint uri that is only a method call, such as from(inboxQueue()), is not validated (it was validated as a property placeholder without a component before); with logUnparseable=true it is logged.
camel-jbang - the route-parser plugin is removed
The route-parser plugin (camel route-parser) and its camel-jbang-plugin-route-parser artifact are removed. Use camel cmd route-structure MyRoute.java instead: it prints the structure of the routes of a Java source, read without compiling it, and of YAML and XML sources too. If you installed the plugin, remove it with camel plugin delete route-parser.
camel-wasm - the endpoint runs WASI command modules (Breaking change)
The wasm endpoint now runs WASI Preview 1 command modules (the body is standard input, standard output becomes the body, the exit code and standard error are in CamelWasmExitCode and CamelWasmStderr) and no longer supports the custom alloc/dealloc and JSON message ABI. The URI path is now the module (wasm:classpath:upper.wasm); the function name and the module option are removed, and a header reaches the module only when listed in environmentHeaders. To migrate, turn the function into a program that reads standard input and writes standard output, built for wasm32-wasip1 (Rust) or with GOOS=wasip1 GOARCH=wasm (Go). The wasm language is unchanged.
The endpoint runs every exchange on a new instance of the module, so a module can no longer keep state from one message to the next. The 4.23 note that a failed call discards the Wasm instance therefore now only applies to the wasm expression.
The following API of camel-wasm is removed or changed with the old ABI:
-
WasmComponent.getModule()andsetModule(String), andWasmConfiguration.getModule()andsetModule(String): the module is the path of the endpoint URI, available asWasmEndpoint.getModule(). -
The
functionNamepath parameter of the endpoint: the third argument of theWasmEndpointconstructor is now the module. -
WasmSupport.deserialize(byte[], Exchange). -
The constructor
WasmProducer(Endpoint, String, String)is replaced byWasmProducer(WasmEndpoint).
camel-wasm has a new dependency, run.endive:compiler, which brings in ASM (org.ow2.asm). It is used by the new compile option of the endpoint (default false). The module is now loaded once per endpoint and no longer by every producer, so stopping and starting a route does not read the module again: remove the route and add it again, or restart the application, to load a module that has changed.
camel-vertx-http - a header with several values is sent as several headers
A message header whose value is a java.util.Collection or an object array (as the HTTP consumers map a request header that occurs more than once) was sent as one HTTP header with the toString() of the value, for example [a, b]. The producer now sends one HTTP header per value. A collection or an object array that is empty, or that holds only null values, now sends no header (an empty List was sent as the header value []).
Every other value is converted to one String and sent as one header as before: a String (also when it contains commas), a byte[] and the other primitive arrays, a Map, and a Vert.x JsonObject or JsonArray (sent as JSON). A route that needs the old single header can set the header to a String, for example with String.valueOf(list).
camel-knative-http - a header with several values is sent as several headers
A message header whose value is a java.util.Collection or an object array (as the consumer maps a request header that occurs more than once) is now sent as one HTTP header per value. The Knative HTTP producer sent it as one HTTP header with the toString() of the value, for example [a, b], and the reply of the consumer sent only its last value. A collection or an object array that is empty, or that holds only null values, sends no header (the producer sent an empty List as the header value []).
Every other value is one header. A String is sent as before, also when it contains commas, and the reply still sends no header for a blank String. A byte[] is now converted to a String with the type converter (the producer sent its toString(), such as [B@1dc9f5ad, and the reply only its last byte as a number). The producer sends a Map and a Vert.x JsonObject or JsonArray as before, as their toString() (JSON for the Vert.x types). The reply now does the same: it sent only the last entry of a Map or JsonObject in the form key=value, and only the last element of a JsonArray.
A route that needs the old single header of the producer can set the header to a String, for example with String.valueOf(list).
camel-http - a header with several values is sent as several headers
A message header whose value is a java.util.Collection or an object array (as the HTTP consumers map a request header that occurs more than once) was sent as one HTTP header with the values in the form [a, b]. The producer now sends one HTTP header per value, as camel-vertx-http and camel-knative-http do. Null values are skipped, and a collection with one value is sent as one header with that value, as before.
Every other value is sent as one header. A String is sent as before, also when it contains commas. A byte[] or a char[] is now converted to a String with the type converter, for example abc (it was sent as [97, 98, 99]), and a Map is sent as its toString(), such as {k1=v1, k2=v2} (it was [k1=v1, k2=v2]). The other primitive arrays, an Iterator, a java.util.stream.Stream and an Iterable that is not a collection are sent in the form [a, b] as before. A route that needs the old single header can set the header to a String, for example with String.valueOf(list).
This also applies to camel-graphql, which sends its requests with camel-http.
camel-dapr - the saveBulk and executeTransaction state operations are sent
The saveBulk and executeTransaction operations of dapr:state built their request but never sent it to Dapr, so nothing was stored and the exchange completed without an error. The requests are now sent, and the producer waits for the answer like the other state operations: the states are stored, and a request that Dapr rejects (or a sidecar that cannot be reached) fails the exchange.
camel-influxdb2 - a write is synchronous and a rejected write fails the exchange
The producer wrote with the batching write API of the InfluxDB client: the point was put into a buffer of the client and sent later by a background thread, so the exchange completed before anything was written, and a write that InfluxDB rejected (a line it cannot parse, an unknown bucket, a token that is not authorized) or could not receive was only logged by the client while the route continued as if the data had been stored. The producer now writes with the synchronous write API: the exchange continues when InfluxDB has answered, and a failed write fails the exchange with a CamelInfluxDb2Exception (cause: the InfluxException of the client), which the error handler can redeliver.
Each exchange is now one write request, where the client used to collect the points of up to one second into one request, and the client no longer retries a write in the background. Routes with a high message rate can send a Points, Measurements or Records body (for example from an aggregator) to write many points with one request.
camel-jira - the producers wait for the answer of Jira
The addComment, addIssueLink, addWorkLog, attach, deleteIssue, transitionIssue and watchers producers (and addIssue for its watchers) sent their request without waiting for the answer, so the exchange continued before Jira had done the work, and a request that Jira rejected (for example no permission, or a transition that is not allowed) was not noticed. They now wait for the answer, as addIssue and updateIssue already did, and a rejected request fails the exchange with the RestClientException of the Jira client. Routes that relied on such failures being ignored must handle the exception, for example with onException(RestClientException.class).handled(true).
A producer that sends several requests can fail after some of them were done: transitionIssue can fail on its comment after the transition was made, watchers and addIssue can fail after some of the watchers were changed (addIssue sets the created issue as the message body before it adds the watchers), and a retry or redelivery repeats the earlier requests.
camel-twitter - twitter-search applies its query options again
Since Camel 4.0 (the upgrade to Twitter4J 4.1, whose Query is immutable) the twitter-search endpoint sent only the keywords. The consumer ignored filterOld (default true), lang, count and the geolocation options, so it routed the same tweets again at every poll. The producer ignored sinceId, lang, count and the CamelTwitterSinceId, CamelTwitterMaxId, CamelTwitterSearchLanguage and CamelTwitterCount headers. The options are applied again: a poll returns only the tweets newer than the last one consumed, and a page has count tweets (default 5; Twitter’s own default of 15 applied before). Raise count and numberOfPages if a search can match more tweets than that between two polls. A consumer with type=direct returned nothing when keywords were given, and now runs the search at startup.
camel-kudu - a row error of Kudu fails the exchange
The insert, delete, update and upsert operations of the producer ignored the answer of Kudu. The Kudu client does not throw for a row that Kudu rejected; it returns a row error, for example for an insert of a key that is already present, or for an update or a delete of a row that is not found. Such an exchange completed as if the row had been written. A write that timed out or could not reach a tablet server was reported the same way, and completed the exchange too; for such a write it is not known whether the row was written. The producer now fails the exchange in both cases with a KuduRowErrorException (getRowError() returns the row error of the Kudu client, whose status tells the kind of error). Routes that relied on rejected rows being skipped silently can use the upsert operation, or handle the exception, for example with onException(KuduRowErrorException.class).handled(true).
The producer also closes the Kudu session of each operation. The sessions stayed registered with the Kudu client until the client was closed, one more for every exchange.
camel-mllp - the producer verifies the host name of the server over TLS
When sslContextParameters is set (on the endpoint, or with useGlobalSslContextParameters on the component), the MLLP producer now checks that the certificate of the server matches the host it connects to, with the same rules as an HTTPS client. Before, it accepted any certificate trusted by the trust store, whatever host the certificate was issued for.
A producer that connects with a host name or an IP address that the certificate does not list now fails the TLS handshake; an IP address must be listed as a subject alternative name of the certificate, and a producer URI without a host name (such as mllp://8888) cannot pass the check. Use a certificate that matches the host of the endpoint, or set the new hostnameVerification option to false to keep the previous behaviour.
camel-atom - sortEntries sorts by the updated date and routes the oldest entry first
With sortEntries=true the atom consumer sorted the entries of a feed by their published date and routed them from the newest to the oldest one. With the default idempotent strategy (which skips an entry that is not newer than the one routed before it) only some entries of a feed were routed, and the others were skipped.
This is fixed. The entries are now sorted by their updated date (by the published date for an entry without an updated date), which is the date the idempotent strategy compares, and they are routed from the oldest to the newest one, as camel-rss does and as Camel 3.x did. Entries are no longer skipped because of the routing order, also when an older entry was updated after newer ones were published.
Routes using sortEntries=true with idempotent=false get the entries in this order too: the entry that was updated first is routed first, where the entry that was published last was routed first before.
camel-aws2-s3 - a listed object that cannot be fetched is skipped
When the consumer lists the bucket, a failure to get one of the listed objects (or to download its content) no longer fails the poll, as long as another object of that listing could be got: the object is skipped, the other listed objects are consumed in that poll, and the skipped object is tried again by a later poll. An object that no longer exists (for example deleted by another consumer after the listing) is skipped with a log line at DEBUG. Any other failure is given to the exception handler of the consumer, with the key in the message: by default that is a WARN with the stack trace, and with bridgeErrorHandler=true the route’s error handler gets it. This happens at every poll that lists an object that can never be read (for example archived, or access denied), until it can be read or is removed from the bucket. Such a poll is a successful poll, so it does not count for backoffErrorThreshold.
The poll still fails, as before, when none of the listed objects could be got and at least one get failed with an error other than that the object no longer exists (for example expired credentials, no permission to get objects, throttling): the failure of the first object is then the failure of the poll, and backoffErrorThreshold applies. In both cases the objects are no longer kept in the inProgressRepository; before, the object (or a new object with the same key) was skipped by all later polls until the endpoint was restarted. With the fileName option, a failed get still fails the poll.
camel-mina - the host name of the server is verified over SSL
When sslContextParameters is set (on the endpoint, or with useGlobalSslContextParameters on the component), the producer, and the consumer with clientMode=true, now check that the certificate of the server matches the host of the endpoint, with the same rules as an HTTPS client. Before, any certificate trusted by the trust store was accepted, whatever host the certificate was issued for.
A connection to a host name or an IP address that the certificate does not list now fails the SSL handshake; an IP address must be listed as a subject alternative name of the certificate. Use a certificate that matches the host of the endpoint, or set the new hostnameVerification option to false to keep the previous behaviour.
MINA does not pass a failed handshake on as an error: the producer fails with a timeout (after writeTimeout, or timeout while waiting for a reply), and the handshake error is logged at ERROR level by org.apache.mina.filter.ssl.SslHandler. A producer that starts timing out after the upgrade should be checked for a certificate that does not list the host.
camel-aws2-kinesis - the consumer continues after the last record when a shard iterator expired
When a cached shard iterator has expired (an iterator is valid for 5 minutes, so after a route was suspended for longer, or with a delay or back-off above 5 minutes), the consumer now continues after the last record it routed from that shard. Previously it requested a new iterator of the configured iteratorType: with TRIM_HORIZON (the default) it routed all the records of the shard again, with LATEST it skipped the records stored meanwhile.
A shard created by a split or a merge while the consumer is running is now read from its first record, whatever the iteratorType. Previously, with LATEST, the records stored in the new shard before the consumer listed it were skipped. The shards that exist when the consumer starts are read from the configured iteratorType, as before.
camel-graphql - the credentials are only sent to the authority of httpUri
The accessToken (with its jwtAuthorizationType) is now added to a request only when the request goes to the scheme, host and port of httpUri, and the username and password are registered for that authority only. Previously neither was bound to the authority of httpUri. A request to another authority, including the same host with another scheme or port, is now sent without them. A route that needs to authenticate to another authority must address it in httpUri. When proxyHost is set, the username and password are no longer offered to the proxy when it asks for authentication.
The producer also no longer takes the request URL from the CamelHttpUri, CamelHttpPath, CamelHttpQuery and other camel-http control headers of the message. It did so since Camel 4.19, when the component moved onto camel-http; the request now always goes to httpUri, as it did before. For the same reason the CamelHttpMethod and CamelHttpProtocolVersion headers no longer apply, and the request is always a POST.
camel-jetty - a multipart text field that occurs several times
A text field of a multipart/form-data request that occurs several times (for example a group of checkboxes) is now mapped to a header whose value is the list of all its values, as a repeated query or form-urlencoded parameter already is. Before, the header had only the first value and the others were lost. Jetty also returns the query string values of the same name, so a name that is in both the query string and a multipart text field is mapped to a list of all these values too, as it already is for a form-urlencoded request.
camel-servlet and camel-jetty - gzip request body that arrives after the headers
The servlet and Jetty consumers uncompressed a request body with Content-Encoding: gzip only when some of its bytes had already arrived when the consumer started to read it. When the client sent the body later than the headers (for example a client that sends Expect: 100-continue), the route got the compressed bytes. Such a body is now uncompressed like any other gzip request body. A route that uncompressed the body itself in that case must no longer do so. An empty body is passed on as before, and so is the body of an exchange with the CamelSkipGzipEncoding property. A request with Content-Encoding: gzip whose body is not gzip data now fails with a ZipException before the route is called (Jetty answers 500) also when the body arrives after the headers, where the route got the bytes as they were sent; it already failed in this way when the body had arrived. With disableStreamCache=true the consumer now waits for the first bytes (the GZIP header) of a gzip request body before it calls the route; with stream caching, the default, it already read the whole body before that. The same applies to every consumer that reads the request body with DefaultHttpBinding.parseBody of camel-http-common, such as the Spring Boot platform-http consumer with useStreaming=true.
camel-undertow - form fields are decoded as UTF-8
The consumer now decodes the fields of an application/x-www-form-urlencoded or multipart/form-data request that declares no charset as UTF-8, the encoding browsers use. Before, Undertow’s default ISO-8859-1 was used, so café became café in the headers and the body map. A charset that the request or a part declares is still used: a client that sends ISO-8859-1 form data without declaring a charset now gets U+FFFD replacement characters instead of its text, and must declare its charset (for example application/x-www-form-urlencoded; charset=ISO-8859-1).
camel-github2 - consumers and items of a poll that were not routed
The github2 consumers (commit, event, pullRequest, pullRequestComment, tag) remembered every item of a poll as consumed before routing it. Items that were not routed because the route was suspended during the poll were never routed. They are now routed by the first poll after the route is resumed.
The commit consumer also lost the new commits of a poll in which the GitHub lookup of an author or a committer failed (for example a 404 for a deleted account, or a server error). Such a commit is now routed, and a WARN is logged for it. Its CamelGitHubCommitAuthor and CamelGitHubCommitCommitter headers then have the name that git recorded in the commit instead of the name of the GitHub profile, and are not set when the commit has no name either.
camel-smpp - the host name of the SMSC is verified over SSL
With smpps (or usingSSL=true), directly or through an HTTP proxy, the connection now checks that the certificate of the SMSC matches the host it connects to, with the same rules as an HTTPS client. Before, any certificate trusted by the JVM was accepted, whatever host it was issued for.
A connection to a host name or an IP address that the certificate does not list now fails the SSL handshake; an IP address must be listed as a subject alternative name of the certificate. Use a certificate that matches the host of the endpoint, or set the new hostnameVerification option to false to keep the previous behaviour.
A mismatch shows as the producer or consumer failing to start, with an IOException whose cause names the host that was checked:
java.io.IOException: No subject alternative names matching IP address 127.0.0.1 found
Caused by: javax.net.ssl.SSLHandshakeException: No subject alternative names matching IP address 127.0.0.1 found camel-netty-http - the request body read as an InputStream
The body of a request received by the consumer is a NettyChannelBufferStreamCache. Its read() method now returns a byte as a value from 0 to 255, and -1 at the end of the body, as java.io.InputStream requires. Before, a byte from 0x80 was returned as a negative value (0xFF as -1, which a reader takes for the end of the body), and the method threw an IndexOutOfBoundsException at the end. markSupported() now returns false (mark(int) never did anything): a reader that needs mark support has to wrap the stream, for example in a BufferedInputStream. reset() goes back to the start of the body, as before.
camel-netty - hostnameVerification verifies the host instead of failing
hostnameVerification=true on a netty producer, or on a consumer with clientMode=true, now checks that the certificate of the server matches the host the endpoint connects to. Before, it made the TLS handshake fail for every certificate, matching or not, with CertificateException: Hostname or IP address is undefined - the engine was created without a peer host, so the JDK had no name to compare against.
A route that had the option set was therefore not connecting at all, and needs no change. A route that sets it while connecting to a host the certificate does not list - most commonly an IP address, which has to appear as a subject alternative name of its own - now fails the handshake with the mismatch as the reason. The default is unchanged: hostnameVerification is false, and the host is then not checked.
Enabling the option also makes the JDK send the host as an SNI server name, as camel-netty-http already did. The default path still creates the engine without a peer and sends no SNI.
On a consumer that acts as a server the option is now ignored instead of being passed to the engine. It never verified anything there - the JDK does not support host name verification on the server side of a handshake, and rejected the client certificate with Endpoint Identification Algorithm HTTPS is not supported on the server side - so a consumer that asks for client certificates and had the option set now completes the handshake instead of failing it.
camel-dropbox - search and get read every page of the result
The search operation (producer and consumer) now returns every match that the service gives. Previously it returned only the first page of the matches (the Dropbox SDK asks for 100 matches per page), and the rest was dropped without any sign. A search with many matches now sends one request per page, for the consumer at each poll. When one of these requests fails, the operation fails and no match is returned, as before.
The get operation on a folder (producer and consumer) now downloads every file of the folder. Previously it downloaded only the files of the first page of the folder listing. As before, the content of all the files is in the message body, so a large folder needs more memory than it did when only the first page was read.
camel-pqc - a configured KeyGenerator or KeyAgreement is used by one exchange at a time
When Camel creates the instance itself, the KEM operations of the pqc component (including the hybrid KEM operations) and the PQC data format now use a new KeyGenerator, and for the hybrid operations a new KeyAgreement, for every operation. A configured instance is still shared: a registry bean, which is autowired into every pqc endpoint, a KeyGenerator set on the data format, or the default material used when no key material is configured. Each operation now holds its lock from init until the result is returned, so concurrent exchanges that use it run one at a time.
A started PQC data format no longer keeps a KeyGenerator of its own: getKeyGenerator() returns only a configured instance and, unless one is configured, the CamelPQCKemAlgorithm header now selects the KEM algorithm of each operation.
camel-box - getEnterpriseEvents returns the events of every chunk
The getEnterpriseEvents operation of the event-logs API now returns all the events between after and before. Previously it returned only the first chunk of the event log (at most 500 events), and the rest was dropped without any sign. A wide after/before window now returns every event in one list: that is one request per 500 events plus one that finds the end of the log, and more memory. Narrow the window when the event log is large.
camel-chunk - message data printed with a direct tag is rendered literally
Previously the chunk component bound the message body, headers and variables into the Chunk engine so that Chunk re-parsed those values as template source: a value that itself contained Chunk tags (such as a body of {$headers.name}) had those tags expanded instead of being printed verbatim.
Message data bound to the engine is now neutralised so that a direct {$tag} printing the body, a header, a variable, or an element of a nested map, list or array renders the value literally instead of evaluating the tags inside it. Tag-free data is unchanged and Chunk’s string filters (for example slice, get, join) keep their existing behaviour. Routes that relied on message data being evaluated as Chunk template code must move that markup into the template itself.
As part of this, a non-List Collection value (such as a Set) is now bound as a List, so a direct {$body} prints Chunk’s list placeholder instead of the collection’s toString(), and a filter such as |join operates on its elements.
This does not cover every path: Chunk re-parses any string it produces while rendering, so the output of a filter ({$body|trim}), a string property of a bean body ({$body.name}, {$exception.message}) and string items bound by a {% loop %} are still parsed as template code. Keep untrusted data to direct {$tag} output; do not pass untrusted values through Chunk filters, bean properties or loops. If untrusted data must go through one of those paths, remove the { and } characters from it in the route before the chunk endpoint (Chunk has no escape for tags inside data), for example with a processor that rewrites the body or header.
camel-azure-servicebus - the dead-letter reason and description are bounded
When the consumer dead-letters a message after a failed route with enableDeadLettering=true, the reason and the error description it writes are now capped at 4096 characters, the limit the Azure SDKs apply to both fields. A value that was cut ends with …. The description holds the stack trace, so a deep one is now truncated rather than sent in full and rejected by the broker.
A settlement request the broker rejects is now reported through the consumer’s exception handler.
camel-telegram - a failed send fails the exchange once
When Telegram answers a message with an error status (for example 400 for an unknown chat), or with a body that cannot be read, the producer now fails the exchange once and the route stops there. Previously the route first continued after the producer as if the message had been sent, and then the exchange failed as well.
The exception of the exchange is the one that process() of the producer threw before: a RuntimeException that wraps an ExecutionException, with the RuntimeCamelException (status and body of the answer) as its cause, or the IOException when the request got no answer. The producer still waits for the answer before it returns, so the order of the messages is the same. The steps after the producer now run on the calling thread, and no longer on a thread of the HTTP client.
camel-vertx-websocket - the client consumer makes one reconnect attempt at a time
When the WebSocket of a consumer with consumeAsClient=true is closed, the consumer now makes one reconnect attempt at a time: while an attempt waits for the server (up to the connect timeout), the next runs of reconnectInterval are skipped, and maxReconnectAttempts counts these attempts one after the other. Previously every interval started another attempt, and when the server answered late each of them opened a WebSocket, so that every message was routed once per WebSocket. A consumer that is stopped no longer reconnects.
camel-core - a route from a route template has the template’s resource as its source
A route created from a route template or a Kamelet now returns the resource with the route template from Route.getSourceResource() (it returned null). This is the resource its source location (Route.getSourceLocation()) was already in, so the debugger and message history consoles show the source code of these routes too.
A route created from a templatedRoute also remembers the resource that declares the templatedRoute (Route.getTemplatedRouteResource(), the TemplatedRouteSource JMX attribute, and templatedRouteSource in the route dev console), and the route template id is available from Route.getRouteTemplateId(), the RouteTemplateId JMX attribute, and routeTemplateId in the route dev console.
Code that took a null source resource to mean that a route was created from a route template should use Route.isCreatedByRouteTemplate() instead.
camel-aws2-ddb - aws2-ddbstream consumer and a suspended route
When a route is suspended (RouteController.suspendRoute, also when a route policy calls it, or CamelContext.suspend()) while the aws2-ddbstream consumer routes the records of a poll, the consumer stops after the record that is being routed. The other records of that poll (up to maxResultsPerRequest, default 100) were skipped when the route was resumed. They are now routed after the resume. A consumer that is suspended directly (consumer.suspend()) routes all the records of its poll and was not affected.
camel-core - the route structure leaves out onException clauses that never run
A route with noErrorHandler runs none of its onException clauses, and a global onException is copied into every route of its route builder or file. The route structure now leaves these clauses out of such routes: the route-structure dev console, camel cmd route-structure, and the route diagrams drawn from it. Before, they were listed among the steps of the route.
camel-diagram - a route diagram draws the steps of a route
A route diagram drew the onException clauses of a route among its steps, though they are not steps: a global onException is copied into every route, and runs only while a failure is handled. A route diagram now draws only the steps of the route: camel cmd route-diagram, the route-diagram dev console and its web component, and the route diagrams dumped by DumpRoutesStrategy. Use --errors (?errors=true in the dev console, the errors attribute of the <camel-route-diagram> web component, errors of DumpRoutesStrategy and @EnableRouteDiagramDump, and dumpRouteDiagramErrors of @CamelMainTest) to draw them in a frame of their own below the steps, with where the failures of the route go. A highlighted path that steps into an onException clause draws the frame as well.
The <camel-topology-diagram> web component no longer draws the error paths of the route topology as calls between routes. With its new errors attribute it draws the routes reached only on error in a frame below the other routes.
camel-jbang - export to Quarkus keeps the names in quarkus.* keys
camel export --runtime=quarkus wrote every quarkus. key except quarkus.hawtio. in dash case: an upper-case letter became a dash and the lower-case letter, and an underscore became a dash. That also changed the names that a key holds, such as a log category, a datasource name or a map key. Now only option names in camel case are written in dash case, such as quarkus.http.rootPath as quarkus.http.root-path. A segment of the key that is quoted, that starts with an upper-case letter, or that has no upper-case letter is kept as it is. So quarkus.log.category."com.example.MyRoute".level, quarkus.datasource.Orders.db-kind and quarkus.datasource.order_history.db-kind are no longer exported as quarkus.log.category."com.example.-my-route".level, quarkus.datasource.-orders.db-kind and quarkus.datasource.order-history.db-kind. The same applies to camel run --runtime=quarkus, which exports the project in the same way.
A name in camel case that starts with a lower-case letter, such as the datasource myOrders or my_DataSource, still looks like an option name and is written in dash case as a whole, underscores included (my-orders, my-data-source). Quote it to keep it: quarkus.datasource."myOrders".db-kind.
camel-mongodb-gridfs - the consumer routes a file once
With the TimeStamp (default) and PersistentTimestamp query strategies, the mongodb-gridfs consumer routed every file uploaded since the consumer started again at each poll (every delay, 500 ms by default). The upload date of the newest routed file is now kept between the polls, so a file is routed once. A route that relied on receiving the same file repeatedly now receives it once. These two strategies now skip a file whose upload date is older than that of a file already routed (as they did before Camel 3.13), for example a file from a second uploader, or from a client with another clock, that becomes visible later: GridFS sets the upload date on the client. Use TimeStampAndFileAttribute or PersistentTimestampAndFileAttribute if such files must be routed: these strategies keep the start date in their query, as before, and mark each file so that it is routed once.
Semantic image input and audit output
Semantic evaluations now recognize a selected ${body} with an image Content-Type, and self-describing data:image/… strings, as image input. Text-only experts reject these inputs instead of treating encoded images as text. For binary image streams, Camel preserves the bytes using a replayable stream cache; downstream code must not rely on the original stream class. For the exact ${body} selector, audio/ and video/ content types now fail before inference, even for a String body. Select an explicitly textual field to evaluate text accompanying audio or video. Content-Type normalization does not apply to other selectors such as $\{bodyAs(byte[])}; use ${body} for image bytes or select a media envelope or image data URL explicitly. Ordinary strings without a media content type remain text. The $semanticInput key is reserved for explicit media envelopes. TypeSafe AI rejects that key in structured state, including nested objects; rename an application-state key that conflicts with it.
SemanticAdapter has an additive default validateState(Object) hook for validation shared by all questions for one expert. Existing per-evaluation validateInput hooks still run; both finish for every expert before any provider performs inference. A shared validation failure marks that expert’s group as invalid input in the audit.
The additive SemanticAdapter.getMaxImageBytes() method exposes an expert’s configured per-image byte limit, with zero meaning no advertised limit. Image-capable adapters should advertise a positive limit and enforce it before inference. CLI and TUI local image loading now requires that metadata; the configured value appears as maxImageBytes in each image operation’s contract.
Semantic audit backends can opt into the additive inputType extension. Readers that do not support it keep their existing projection. The camel semantic audit --json display omits image base64 and reports metadata instead. Use --output=<new-file> to export the full returned JSON, with --include-input for case/bulk requests that need captured input. Existing files are not overwritten.
camel-milo - creating and deleting the monitored items of a consumer
Since 4.16, a milo-client consumer whose monitored item could not be created, for example because the node does not exist, received nothing. Now it receives one DataValue with the bad status code (when omitNullValues=false). The monitored item is created again when the route is restarted or when Camel rebuilds the connection to the server; unlike Camel 3.x, it is not retried every 10 seconds.
The consumer now sets the CamelMiloNodeId header on every exchange, including a consumer with a single node.
Stopping a consumer now deletes its monitored items. Previously they stayed on the server while another endpoint kept the shared connection open, so a restarted consumer received each value twice. While the server is unreachable, the stop waits until the client’s next attempt to reconnect has failed.