Hi Marcus, Thanks for the thorough analysis — it's spot-on.
The main issue (exchange properties not copied in createDummy) was already fixed and backported to 4.18.x and 4.14.x. Your report also uncovered a remaining gap in the useList=false path for FTP/SFTP where pollNamedFile() used a completely empty exchange — that fix is now up as well: https://github.com/apache/camel/pull/25031 Both fixes will be in the next patch releases. On Wed, Jul 22, 2026 at 10:04 AM Marcus Ionker <[email protected]> wrote: > > Hello Claus, > First of thanks for the quick response. > My colleague and I did some further analysis of the issue and we suspect > that there are more facets to it. See below the analysis prepared with the > assistance of AI. > > ------------------------- > > # Camel issue draft > > > File at https://github.com/apache/camel/issues (Apache Camel now uses > GitHub Issues). > > Components: camel-core (PollEnricher / PollDynamicAware), camel-file, > camel-ftp. > > ## Title > Dynamic `pollEnrich` `fileName` referencing an exchange property is > silently ignored; result depends on `allowOptimisedComponents` (documented > as an optimisation only) > > ## Affects version(s) > - Reproduced on **4.14.7**. Likely all versions since **4.11.0** (when > `PollDynamicAware` was introduced — CAMEL-21733). > - Java 21. > > ## Summary > When `pollEnrich` uses a dynamic endpoint URI whose `fileName` references > an **exchange > property**, the filter silently fails to match the intended file. The > behaviour depends on > `allowOptimisedComponents` (default `true`), which is documented purely as > a performance > optimisation — yet toggling it changes the **result**: > > - `allowOptimisedComponents(true)` (default): the wrong file is picked, or > none is found. > - `allowOptimisedComponents(false)`: works correctly. > > The same `fileName` referencing a **header** works with the optimisation > on (for the directory-LIST > path), but a header does **not** work on the FTP `useList=false` path. > This asymmetry is undocumented > and the failure is silent (no warning/exception). > > ## Steps to reproduce (camel-file, no server needed) > Two files exist in `target/in`: `aaa.txt` and `wanted.txt`. > > ```java > from("direct:start") > .setProperty("myFile", constant("wanted.txt")) > // dynamic pollEnrich; fileName references an EXCHANGE PROPERTY > .pollEnrich() > > .simple("file:target/in?fileName=${exchangeProperty.myFile}&noop=true&idempotent=false") > .cacheSize(1) > .timeout(2000) > .to("mock:result"); > ``` > > Send one message to `direct:start` and inspect the enriched body / > `CamelFileName`. > > ## Expected > The enricher retrieves `wanted.txt` (the file named by the exchange > property). > > ## Actual > - With `allowOptimisedComponents(true)` (default): the enricher retrieves > **`aaa.txt`** (the first > file returned by the directory scan) — the `fileName` filter is silently > ignored. > - Change the expression to a **header** (`${header.myFile}` + > `.setHeader("myFile", ...)`): now it > correctly retrieves `wanted.txt` with the optimisation on. > - Add `.allowOptimisedComponents(false)`: the original > **exchange-property** version also correctly > retrieves `wanted.txt`. > > ### FTP variant (camel-ftp) > On the FTP consumer the failure mode also depends on `useList`: > - `useList=true`: as above, the first listed file is taken (filter > ignored). > - `useList=false`: the poll finds no file at all and times out (`fileName` > resolves empty), for > **both** exchange-property and header expressions. > > ## Root cause (as read in 4.14.7 sources) > 1. With the optimisation on, > `GenericFilePollDynamicAware.resolveStaticUri()` re-injects the > *original, unresolved* `fileName` expression into a shared static > endpoint, so `fileName` is > evaluated **lazily** by the consumer. > 2. The consumer evaluates `fileName` against a **dummy** exchange: > - `useList=true` → `GenericFileConsumer.isMatched()` → > `GenericFileHelper.createDummy(endpoint, dynamic, file)`, which > copies only **headers** and > **variables** from the routing exchange — **not exchange properties**: > ```java > if (dynamic.getMessage().hasHeaders()) { > MessageHelper.copyHeaders(dynamic.getMessage(), > dummy.getMessage(), true); > if (dynamic.hasVariables()) { > dummy.getVariables().putAll(dynamic.getVariables()); > } > } > ``` > So `${exchangeProperty.X}` resolves to `null`. And because > `isMatched` skips the `fileName` > filter when it evaluates to `null` (`if (result != null) { if > (!name.equals(result)) ... }`), > **all** files match and the first is taken. > - `useList=false` → `FtpConsumer.pollNamedFile()` evaluates the > expression against > `ExchangeHelper.getDummy(context)` — a **completely empty** exchange > (no headers, variables, or > properties) — so the name resolves empty and no file is polled. > 3. With `allowOptimisedComponents(false)`, `PollEnricher` resolves the URI > expression **eagerly > against the routing exchange**, yielding a literal `fileName`, which > works regardless of `useList` > and regardless of whether the source was a header or a property. > > ## Why this is a bug (or at least should change) > 1. `allowOptimisedComponents` is documented as an optimisation; > enabling/disabling it should not > change results. Here it silently changes correctness. > 2. The optimised path **silently** produces a wrong result (wrong file, or > no file) when the dynamic > `fileName` references data it cannot resolve, instead of failing fast > or falling back. > 3. `createDummy` copies headers **and** variables but **not** exchange > properties — an undocumented > asymmetry. Separately, variables are only copied when > `dynamic.getMessage().hasHeaders()` is true, > which looks like a latent bug (a message with variables but no headers > won't have its variables > carried over). > 4. `isMatched` treating an empty/`null` `fileName` as "match everything" > turns a mis-resolved > dynamic filename into "grab the first file", which is a surprising and > dangerous default for a > named-file poll. > > ## Suggested fix (any of) > - Include exchange properties in `GenericFileHelper.createDummy` > carry-over (and fix the > variables-only-if-headers nesting), so property-based `fileName` > expressions behave like > header-based ones. > - Or have `GenericFilePollDynamicAware` detect that the `fileName` > expression references > data it does not support on the lazy path (e.g. > `${exchangeProperty...}`) and skip the > optimisation for that endpoint (fall back to eager resolution) rather > than silently no-op. > - At minimum: document the limitation (only headers/variables are > supported in an optimised dynamic > `pollEnrich` `fileName`) and log a WARN when a dynamic `fileName` > resolves to empty/null on the > poll path. > > ## Workaround > Set `.allowOptimisedComponents(false)` on the `pollEnrich`, which resolves > the dynamic URI eagerly > against the routing exchange into a literal `fileName`. > > > > > Marcus Ionker > Senior Product Evangelist, Messaging > > > Fax: +49 89 1250400-1202 > Mobile: +49 176 15528012 > Email: [email protected] > > -------------------------------------------------------------------- > > retarus GmbH. Aschauer Straße 30, 81549 München, Germany. > https://www.retarus.com > On 2026/07/21 14:01:54 Marcus Ionker wrote: > > Hello Apache Camel Developers, > > > > I suspect a bug GenericFilePollDynamicAware.resolveStaticUri. I did not > find an existing Jira ticket in ASF pertaining to this issue.I prepared the > following ticket, which contains a detailed description of the problem. I > hope I have not overlooked anything. My current workaround is to set > allowOptimisedComponents to false. Maybe the problem is related to the fact > that although the file endpoints (incl. ftp, fps and sftp) support > expressions in the URI, they are resolved with an empty dummy exchange, > rather than an actual exchange. > > > > Best Regads, > > Marcus Ionker > > > > ---------------------------- > > > > Bug: > > > > GenericFilePollDynamicAware.resolveStaticUri incorrectly returns static > URI for dynamic fileNameexpressions, breaking PollEnricher optimization > > > > Description: > > > > There is an optimization regression in > org.apache.camel.component.file.GenericFilePollDynamicAware when used > alongside pollEnrich. > > > > When a pollEnrich endpoint contains a dynamic simple expression > targeting a parameter like fileName (e.g., in a File or FTP/SFTP endpoint), > GenericFilePollDynamicAware.resolveStaticUri fails to return null. Other > implementations of PollDynamicAwareSupport correctly yield null when a > component configuration is dynamic. Because it incorrectly evaluates to a > non-null static URI base, PollEnricher.java falls into an invalid > optimization block. > > > > Root Cause Analysis: > > > > GenericFilePollDynamicAware.resolveStaticUri is returning the resolved > dynamic URI instead of null > > > > if (fileName) { > > Map<String, Object> params = entry.getProperties(); > > Map<String, Object> originalParams = > URISupport.parseQuery(URISupport.extractQuery(entry.getOriginalUri())); > > compute(originalParams, PROP_FILE_NAME, params); > > return asEndpointUri(exchange, uri, params); > > } else { > > return uri; > > } > > > > Inside org.apache.camel.processor.PollEnricher.process(), the framework > attempts to evaluate and optimize the dynamic endpoint. > > > > try { > > recipient = expression.evaluate(exchange, Object.class); > > if (dynamicAware != null) { > > // if its the same scheme as the pre-resolved dynamic aware then > we can optimise to use it > > String originalUri = uri; > > String uri = resolveUri(exchange, recipient); > > String scheme = resolveScheme(exchange, uri); > > if (dynamicAware.getScheme().equals(scheme)) { > > PollDynamicAware.DynamicAwareEntry entry = > dynamicAware.prepare(exchange, uri, originalUri); > > if (entry != null) { > > staticUri = dynamicAware.resolveStaticUri(exchange, entry); > > if (staticUri != null) { > > if (LOG.isDebugEnabled()) { > > LOG.debug("Optimising poll via PollDynamicAware > component: {} to use static uri: {}", scheme, > > URISupport.sanitizeUri(staticUri)); > > } > > } > > } > > } > > } > > }... > > > > > > The Breakdown: > > > > - expression.evaluate(...) correctly resolves the dynamic endpoint. > > > > - However, because GenericFilePollDynamicAware.resolveStaticUri provides > a fallback staticUri string rather than null, PollEnricher assumes it can > safety optimize the consumer. > > > > - Instead of using the fully evaluated runtime endpoint string > (recipient), the route erroneously falls back to using the unparsed simple > expression itself as the literal target. > > > > - The FTP/File consumer subsequently tries to poll using the literal > expression text, resulting in a lookup failure because the string syntax > does not match an actual file name on the remote target file system. > > > > Current Workaround: > > > > Configuring the EIP via > .pollEnrich().simple("...").allowOptimisedComponents(false)bypasses > PollDynamicAware entirely, forcing Camel to correctly interpret the parsed > expression string on every exchange. > > > > Expected Fix: > > > > GenericFilePollDynamicAware.resolveStaticUri must return null if the URI > is dynamic to disable optimization, mirroring standard > PollDynamicAwareSupport behavior. > -- Claus Ibsen
