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

Reply via email to