Skip to content

CAMEL-24435: camel-as2 - clear the per-request asynchronous MDN state on the connection context - #26207

Open
oscerd wants to merge 1 commit into
apache:mainfrom
oscerd:fix/CAMEL-24435
Open

CAMEL-24435: camel-as2 - clear the per-request asynchronous MDN state on the connection context#26207
oscerd wants to merge 1 commit into
apache:mainfrom
oscerd:fix/CAMEL-24435

Conversation

@oscerd

@oscerd oscerd commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Issue

CAMEL-24435

Problem

AS2ServerConnection.RequestHandlerThread.run() creates its HttpContext once, outside the
request loop, and reuses it for every request handled on that connection:

final HttpContext context = HttpCoreContext.create();
while (!Thread.interrupted()) {
    this.httpService.handleRequest(this.serverConnection, context);
    ...
    String recipientAddress = coreContext.getAttribute(AS2AsynchronousMDNManager.RECIPIENT_ADDRESS, String.class);
    if (recipientAddress != null && config != null) {
        // Send the MDN asynchronously.

ResponseMDN sets RECIPIENT_ADDRESS and ASYNCHRONOUS_MDN on that context while handling a request
that carried Receipt-Delivery-Option, and nothing removed them afterwards. A later request on the
same connection that did not ask for an asynchronous receipt therefore still found a non-null
recipient address, and the handler dispatched a second asynchronous MDN — carrying whatever report was
still stored under ASYNCHRONOUS_MDN — to the address supplied by the earlier request.

Fix

Both attributes are removed in a finally after every request. Using finally rather than clearing
at the end of the send block also covers the paths that skip the send — in particular a request URI
with no registered consumer configuration, where config is null and the old code left the address
behind untouched.

Also clears CURRENT_CONSUMER_CONFIG

The same finally clears the CURRENT_CONSUMER_CONFIG ThreadLocal, which is not in the issue text
but has the identical defect. setupConfigurationForRequest returns early without setting it when
a path has no registered configuration:

if (config == null) {
    LOG.warn("No AS2 consumer configuration found for canonical path: {} ...", requestUriPath);
    return null;
}
...
CURRENT_CONSUMER_CONFIG.set(wrapper);

so the thread kept the previous request's wrapper. On its own that cannot trigger a send, but paired
with a new legitimate recipient address it would have signed the MDN with the previous path's key.
Clearing only the context attributes would have left that half of the leak in place.

The post-processing block is extracted into handleAsynchronousMDN so the finally reads clearly;
the logic inside is unchanged.

Test

AS2AsyncMdnContextReuseTest sends two messages through one as2://client/send route, so both travel
over the same pooled connection to the server:

  1. the first carries receiptDeliveryOption → exactly one MDN must reach the receipt endpoint;
  2. the second does not → the receipt endpoint must see no further MDN.

The second assertion uses setAssertPeriod(5s) so a leaked delivery has time to arrive rather than
the test passing simply by being quick.

Verified both ways: against the unfixed camel-as2-api the test fails with
mock://receipts Received message count. Expected: <0> but was: <1>, and it passes with the fix.

components/camel-as2 suite green (API + Component). Full reactor build clean.

Documentation

No upgrade-guide entry: the previous behaviour was sending an MDN nobody asked for, to an address from
an unrelated earlier request. There is nothing for a user to migrate.


Claude Code on behalf of oscerd

🤖 Generated with Claude Code

… on the connection context

The AS2 request handler creates its HttpContext once, outside the request loop,
and reuses it for every request handled on that connection. ResponseMDN sets
RECIPIENT_ADDRESS and ASYNCHRONOUS_MDN on it while handling a request that asked
for an asynchronous receipt, and nothing removed them afterwards. A later
request on the same connection that did not ask for one therefore still found a
non-null recipient address, and the handler dispatched a second asynchronous MDN
- carrying the report still stored under ASYNCHRONOUS_MDN - to the address
supplied by the earlier request.

Both attributes are now removed in a finally block after every request, so the
clearing also covers the paths that skip the send, such as a request URI with no
registered consumer configuration.

The same finally clears CURRENT_CONSUMER_CONFIG. setupConfigurationForRequest
returns early without setting it when a path has no configuration, so the
ThreadLocal had the identical staleness: a request could otherwise pair its own
recipient address with the previous request's signing key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEkdQs2n6dsP4SFTcfG8rB
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
@oscerd
oscerd requested review from davsclaus and gnodet September 8, 2026 12:30

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is correct and the test structure is sound. Two observations below.


Static analysis note: ast-grep flagged an empty catch (IOException ignore) on the serverConnection.close() call in the outer finally block (line 534 in the PR file) and semgrep flagged a plain ServerSocket instantiation at line 448. Both are pre-existing, not introduced by this PR — disregarding.


This review was generated by an AI agent, Hermès, on behalf of @gnodet.

receipts.expectedMessageCount(1);
requestBodyAndHeaders("direct://SEND", EDI_MESSAGE,
as2Headers("http://localhost:" + jettyPort.getPort() + "/handle-receipts"));
receipts.setResultWaitTime(TimeUnit.SECONDS.toMillis(10));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Test ordering hazard: setResultWaitTime is set after the requestBodyAndHeaders call that triggers the MDN. If the MDN arrives and the latch reaches zero between line 79 and line 80, assertIsSatisfied on line 81 will still work (latch is already at zero, so it returns immediately without waiting), but the wait-time guarantee is meaningless from a correctness standpoint. It cannot cause a false positive here — expectedMessageCount(1) is already satisfied by that point — but the conventional pattern is to configure the endpoint before triggering the action:

Suggested change
receipts.setResultWaitTime(TimeUnit.SECONDS.toMillis(10));
receipts.setResultWaitTime(TimeUnit.SECONDS.toMillis(10));
requestBodyAndHeaders("direct://SEND", EDI_MESSAGE,
as2Headers("http://localhost:" + jettyPort.getPort() + "/handle-receipts"));
receipts.assertIsSatisfied();

}

@Test
public void asyncMdnStateDoesNotLeakToTheNextRequestOnTheSameConnection() throws Exception {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Test classification: The class name ends in Test, so it runs under maven-surefire as a unit test. The parent POM excludes **/*IT.java from surefire and routes those to failsafe. Other AS2 async MDN tests (AS2AsyncMDNServerManagerIT) use the IT suffix. Since this test is self-contained (sets up its own AS2ServerConnection in setupResources), running under surefire is fine — the test does not require the failsafe lifecycle. Just confirming this is intentional and not a naming oversight.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-as2/camel-as2-api
  • components/camel-as2/camel-as2-component

🔬 Scalpel shadow comparison — Scalpel: 10 tested, 27 compile-only — current: 10 all tested

Maveniverse Scalpel detected 37 affected modules (current approach: 10).

⚠️ Modules only in Scalpel (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

Skip-tests mode would test 10 modules (2 direct + 8 downstream), skip tests for 27 (generated code, meta-modules)

Modules Scalpel would test (10)
  • camel-as2
  • camel-as2-api
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-launcher-container
  • camel-yaml-dsl-validator
  • camel-yaml-dsl-validator-maven-plugin
Modules with tests skipped (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (37 modules, 5m 10s total)

Total reactor time: 5m 10s

Module Duration Status
Camel :: Launcher 49.5s SUCCESS
Camel :: AS2 :: Component 46.3s SUCCESS
Camel :: JBang :: MCP 37.5s SUCCESS
Camel :: JBang :: Plugin :: TUI 21.0s SUCCESS
Camel :: Component DSL 19.6s SUCCESS
Camel :: AS2 :: API 16.9s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 16.3s SUCCESS
Camel :: Catalog :: Camel Catalog 14.5s SUCCESS
Camel :: Docs 14.3s SUCCESS
Camel :: Kamelet Main 8.7s SUCCESS
Camel :: Catalog :: Camel Route Parser 8.4s SUCCESS
Camel :: JBang :: Plugin :: Testing 7.8s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 7.6s SUCCESS
Camel :: YAML DSL :: Deserializers 6.1s SUCCESS
Camel :: All Components Sync point 5.7s SUCCESS
Camel :: YAML DSL :: Validator 4.8s SUCCESS
Camel :: YAML DSL :: Maven Plugins 3.2s SUCCESS
Camel :: Catalog :: Suggest 3.0s SUCCESS
Camel :: Catalog :: Maven 3.0s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 2.4s SUCCESS
Camel :: Assembly 1.7s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.7s SUCCESS
Camel :: Coverage 1.6s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.4s SUCCESS
Camel :: Catalog :: Dummy Component 1.2s SUCCESS
Camel :: JBang :: Main 1.2s SUCCESS
Camel :: JBang :: Plugin :: Validate 0.9s SUCCESS
Camel :: JBang :: Plugin :: Route Parser 0.8s SUCCESS
Camel :: Endpoint DSL :: Support 0.8s SUCCESS
Camel :: Catalog :: Console 0.7s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: JBang :: Integration tests 0.7s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.5s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: YAML DSL n/a

Top 20 slowest modules:

  • Camel :: Launcher (49.5s)
  • Camel :: AS2 :: Component (46.3s)
  • Camel :: JBang :: MCP (37.5s)
  • Camel :: JBang :: Plugin :: TUI (21.0s)
  • Camel :: Component DSL (19.6s)
  • Camel :: AS2 :: API (16.9s)
  • Camel :: JBang :: Plugin :: Kubernetes (16.3s)
  • Camel :: Catalog :: Camel Catalog (14.5s)
  • Camel :: Docs (14.3s)
  • Camel :: Kamelet Main (8.7s)
  • Camel :: Catalog :: Camel Route Parser (8.4s)
  • Camel :: JBang :: Plugin :: Testing (7.8s)
  • Camel :: Catalog :: Camel Report Maven Plugin (7.6s)
  • Camel :: YAML DSL :: Deserializers (6.1s)
  • Camel :: All Components Sync point (5.7s)
  • Camel :: YAML DSL :: Validator (4.8s)
  • Camel :: YAML DSL :: Maven Plugins (3.2s)
  • Camel :: Catalog :: Suggest (3.0s)
  • Camel :: Catalog :: Maven (3.0s)
  • Camel :: YAML DSL :: Validator Maven Plugin (2.4s)

⚙️ View full build and test results

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants