Skip to content

CAMEL-24499: camel-spring-boot - route detail view bypasses the route.start.exception serialization filter - #1898

Merged
oscerd merged 2 commits into
apache:mainfrom
oscerd:fix/CAMEL-24499
Aug 27, 2026
Merged

CAMEL-24499: camel-spring-boot - route detail view bypasses the route.start.exception serialization filter#1898
oscerd merged 2 commits into
apache:mainfrom
oscerd:fix/CAMEL-24499

Conversation

@oscerd

@oscerd oscerd commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

RouteEndpointInfo annotates its properties map so route.start.exception is not serialized:

@JsonIgnoreProperties(value = {"route.start.exception"})
private final Map<String, Object> properties;

RouteDetailsEndpointInfo extends it but re-declared the same field without the annotation, plus a
getProperties() override, so the detail operation serialised the property the base view filters.

Both the annotation and the shadowing field arrived in the same commit under CAMEL-20993 — the filter was
clearly intended, and the subclass field simply escaped it. The shadow and its getter are removed; the
annotated base-class property is used instead.

route.start.exception holds the actual Throwable (InternalRouteStartupManager puts it there), so Jackson
was serialising the whole object graph — nested causes, every stack frame with class/file/line, classloader
names and the JDK version.

No public API change: RouteDetailsEndpointInfo.getProperties() was an override of a method the public
base class RouteEndpointInfo also declares, so it stays available on the subclass by inheritance — source
and binary compatible.

Tests

CamelRoutesEndpointStartExceptionTest — 2 cases over the existing controlled-bar fixture (a route that
fails to start): neither the info nor the detail view serialises route.start.exception, and both still
serialise the benign route properties.

The supervising controller starts routes asynchronously, so a @BeforeEach uses Awaitility to wait until the
raw route properties actually hold a Throwable under route.start.exception before either view is built —
otherwise the assertions could pass with nothing to leak.

Verified meaningful: the detail case fails against the unpatched code and passes with it; the info case passes
either way, which also confirms @JsonIgnoreProperties genuinely filters map entries, so removing the shadow
is sufficient. Full core/camel-spring-boot suite: 138 tests, 0 failures.

Changed after review

This PR originally also restricted the health-check error.stacktrace detail to the full exposure level.
That has been dropped, after @luigidemasi pointed out it was not accidental placement:

  • CAMEL-18832 created CamelHealthHelper explicitly to "align output to be similar to microprofile-health",
    and its original implementation already put the error block outside the exposure-level branch — the full
    check governs metadata filtering of the details map, not the error block.
  • camel-microprofile-health's testExposureLevelDefault asserts error.stacktrace is present at the
    default level.
  • The exposure-level docs say default "only include detailed information for health check that was DOWN".

Restricting it only in Spring Boot would silently diverge the two runtimes. The unauthenticated-exposure
concern is real but belongs with CAMEL-24498, where the observability starter raises
camel.health.exposure-level to full and show-details to always on an all-interfaces listener — at
Spring Boot's own default (show-details=never) these details are not exposed at all.

The error.message overwrite across multiple DOWN checks that @luigidemasi also found is a pre-existing
defect independent of this PR, now tracked as
CAMEL-24512.


Filed by Claude Code on behalf of Andrea Cosentino.

…ws honour the configured exposure

Only emit error.stacktrace in the health details at the 'full' exposure level;
at the default level report the exception type alongside the existing message
and keep the full trace in the server log.

Remove the properties field and getter that RouteDetailsEndpointInfo declared
on top of the annotated RouteEndpointInfo property, which bypassed the
route.start.exception serialization filter added in CAMEL-20993.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>

@davsclaus davsclaus 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.

Reviewed against this project's build/test/contribution rules. I checked out the branch locally, built the module, ran the new tests, and — importantly — reverted the two fix files back to main while keeping the new tests to confirm they actually catch the described regressions before trusting the PR's own claims about them.

Verification performed

  • Build: mvn -DskipTests install in core/camel-spring-boot succeeds.
  • New tests: CamelHealthHelperExposureTest (3 cases) and CamelRoutesEndpointStartExceptionTest (2 cases) all pass on the PR branch.
  • Regression check: reverted CamelHealthHelper.java and CamelRoutesEndpoint.java to main while keeping the new tests — both defect tests failed. The failure output is a striking confirmation of the bugs: the health check test leaked a full stack trace at the default exposure level, and the route detail test serialized the entire route.start.exception object graph (nested causes, every stack frame with classloader name, module name, JDK version, etc.), exactly as described in the PR body. Restored the fix and confirmed green again.
  • Full suite: mvn test in core/camel-spring-boot — 141 tests, 0 failures, matching the PR description exactly.
  • History check: git show ebccbfe5408 (CAMEL-20993) confirms the @JsonIgnoreProperties(value = {"route.start.exception"}) annotation and the shadowing properties field in RouteDetailsEndpointInfo were introduced in the same commit — the filter escaping the subclass looks like an accidental oversight, not an intentional design choice being reverted.
  • "No public API change" claim: verified the public base class RouteEndpointInfo still declares both the annotated field and getProperties(), so removing the subclass's duplicate override is source/binary compatible as claimed.

Findings

No blocking issues. This is a clean, well-scoped, well-verified information-disclosure fix.

  • Both root causes are independently verified as real defects, not reverts of intentional behavior.
  • Test coverage is genuinely meaningful — empirically confirmed the tests fail without the fix.
  • Single commit, correctly formatted per project guidelines (CAMEL-24499: ...).
  • No public API changes, no new dependencies, no Lombok/Records — compliant with project standards.
  • Tight scope: two related root causes in the same actuator exposure area, no drift into unrelated code.

No nitpicks or open questions on this one.

This review does not replace specialized tools like CodeRabbit, Sourcery, or SonarCloud/static analysis.

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

@luigidemasi

Copy link
Copy Markdown
Contributor

Review verdict: Request changes

Reviewed exact head 2f56679689c5811227e890dd8ee5b3ea173a5a7d against base 438b015ea3aaff65cca047a318f9385e835bf18e.

Findings

[P1] Do not silently redefine the established default health-exposure contract

[CamelHealthHelper.java:62-82](https://github.com/apache/camel-spring-boot/blob/2f56679689c5811227e890dd8ee5b3ea173a5a7d/core/camel-spring-boot/src/main/java/org/
apache/camel/spring/boot/actuate/health/CamelHealthHelper.java#L62-L82)

CAMEL-18832 introduced this helper to align Spring Boot with MicroProfile health. MicroProfile’s default-exposure regression still explicitly requires
error.stacktrace, while Camel’s documentation says default includes detailed information for failed checks.

This PR removes that longstanding JSON field only from Spring Boot and characterizes its placement as accidental without acknowledging the compatibility change or cross-
runtime divergence.

References:

Either retain the established default behavior or explicitly approve and document the compatibility change, including the resulting MicroProfile divergence.

[P2] Preserve the message for every failing health check

[CamelHealthHelper.java:63-77](https://github.com/apache/camel-spring-boot/blob/2f56679689c5811227e890dd8ee5b3ea173a5a7d/core/camel-spring-boot/src/main/java/org/
apache/camel/spring/boot/actuate/health/CamelHealthHelper.java#L63-L77)

Default exposure can return multiple DOWN results. The indicator applies them to one Health.Builder, but each result writes to the same top-level error.message.
Spring Boot uses Map.put, so the last message replaces every earlier one.

An exact-head probe with two failures produced:

error.message=second-message
first.data={error.type=IllegalArgumentException}
second.data={error.type=IllegalStateException}

first-message is lost. Before this PR, it remained available inside first.data.error.stacktrace.

Store error.message inside each check-scoped data map—retaining the top-level key if compatibility requires it—and add a regression test with two distinct DOWN
results.

References:

[P2] Establish the asynchronous route-failure precondition in the test

[CamelRoutesEndpointStartExceptionTest.java:33-70](https://github.com/apache/camel-spring-boot/blob/2f56679689c5811227e890dd8ee5b3ea173a5a7d/core/camel-spring-boot/
src/test/java/org/apache/camel/spring/boot/actuate/endpoint/CamelRoutesEndpointStartExceptionTest.java#L33-L70)

The supervising controller starts asynchronously after a configured 100 ms delay. The tests immediately serialize the route and only assert that route.start.exception
is absent.

If serialization occurs before the failed start installs that property, both tests pass against the unfixed implementation without exercising the leak.

Use Awaitility with an explicit atMost timeout to:

  1. Wait until the raw route properties contain a Throwable under route.start.exception.
  2. Build the endpoint view.
  3. Verify serialization omits that key while preserving benign properties.

[P3] Remove or implement the server-log guarantee

[CamelHealthHelper.java:66-76](https://github.com/apache/camel-spring-boot/blob/2f56679689c5811227e890dd8ee5b3ea173a5a7d/core/camel-spring-boot/src/main/java/org/
apache/camel/spring/boot/actuate/health/CamelHealthHelper.java#L66-L76)

The comment states that the full trace remains in the server log, but neither the helper nor its caller logs or attaches the throwable using down(Throwable).

Spring Boot only logs exceptions attached to its builder or thrown by the indicator. Some Camel health checks merely store exceptions in their result, so the trace is
not guaranteed to exist in server logs.

[Spring Boot exception-logging path](https://github.com/spring-projects/spring-boot/blob/v4.1.0/module/spring-boot-health/src/main/java/org/springframework/boot/health/
contributor/AbstractHealthIndicator.java#L76-L93)

Remove or qualify the claim unless intentional, non-repetitive logging is implemented.

Verification

  • Exact PR head: both new test classes passed — 5 tests, 0 failures.
  • The same tests applied to the base produced the two intended regression failures.
  • All hosted CI checks currently pass.
  • The route DTO implementation itself behaves correctly under Jackson 2 and Jackson 3.
  • The inherited getProperties() remains source- and binary-callable.
  • No code, PR metadata, comments, or reviews were modified during this review.

This review does not replace static analysis or specialized review tooling.

Generated by Codex on behalf of Luigi De Masi. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Drop the health-check exposure change. CAMEL-18832 placed the error block
outside the exposure-level branch deliberately, to align the Spring Boot output
with camel-microprofile-health, whose testExposureLevelDefault asserts
error.stacktrace is present at the default level. Restricting it only here would
diverge the two runtimes; the unauthenticated-exposure concern belongs with
CAMEL-24498, which is what raises exposure-level to full.

Establish the asynchronous start-failure precondition in the route test with
Awaitility, so the assertions cannot pass vacuously before the supervising
controller records route.start.exception, and assert the benign route properties
are still serialized.

The error.message overwrite across multiple DOWN checks is tracked separately as
CAMEL-24512.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
@oscerd oscerd changed the title CAMEL-24499: camel-spring-boot - actuator health and route detail views honour the configured exposure CAMEL-24499: camel-spring-boot - route detail view bypasses the route.start.exception serialization filter Aug 26, 2026
@oscerd

oscerd commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @luigidemasi — this was a good catch on P1, and I checked each point rather than taking them as read. Head is now 85b446273ae.

[P1] Default health-exposure contract — agreed, change dropped.

You're right, and my framing of it as accidental placement was wrong. I verified it: the original CAMEL-18832 implementation (22a6e0429b9) already had the error block outside the exposure-level branch — the full check governs metadata filtering of the details map, not the error block. The commit message says "Aligned output to be similar to microprofile-health", and camel-microprofile-health's testExposureLevelDefault does assert error.stacktrace at the default level. The docs agree: default "only include detailed information for health check that was DOWN".

I checked git history for the route-detail half (CAMEL-20993) but not for the health half, which is exactly how I ended up reverting an intentional decision. The health change is removed from this PR, along with its test; the PR is now the route-detail fix only.

The underlying exposure concern is real but belongs with CAMEL-24498, where the observability starter raises camel.health.exposure-level to full and show-details to always on an all-interfaces listener. At Spring Boot's own default (show-details=never) these details are not exposed at all. If the cross-runtime default should change, that needs a coordinated change with camel-microprofile rather than a unilateral one here — happy to open that if you and @davsclaus think it's worth it.

[P2] error.message overwrite — confirmed, filed separately.

Verified: CamelHealthCheckIndicator.doHealthCheck passes the same Health.Builder to applyHealthDetail for every result, and error.message is a flat top-level key, so Map.put semantics mean the last DOWN check wins. It pre-exists on main and is independent of this PR, so rather than widen this changeset I've filed CAMEL-24512 with your reproduction and the suggested fix (move it into the check-scoped data map, regression test with two DOWN results).

[P2] Asynchronous route-failure precondition — fixed.

Fair point. The tests did fail against the unfixed implementation when I ran them, but that was timing-dependent rather than guaranteed. A @BeforeEach now uses Awaitility (atMost 30s) to wait until the raw route properties actually hold a Throwable under route.start.exception before either view is built, and both tests additionally assert the benign properties are still serialized. Re-verified against the base: the detail test still fails there, so the regression coverage is intact.

[P3] Server-log guarantee — gone with the health change.

The comment making that claim was part of the reverted hunk, so it no longer exists. You were right that nothing guaranteed it.

Full core/camel-spring-boot suite after the changes: 138 tests, 0 failures. I've left the conversations open for you to resolve.

@oscerd
oscerd requested a review from davsclaus August 26, 2026 11:31

@luigidemasi luigidemasi 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.

Thanks for addressing the feedback. I rechecked exact head 85b446273ae0aba20f749e5fbf08f6a83f58d23e.

The PR-specific findings are resolved:

  • The incompatible health-exposure change and unsupported logging claim were removed.
  • The Awaitility precondition makes the route regression test non-vacuous.
  • The updated test passes on the PR head and fails as expected on the base implementation.
  • The pre-existing multi-DOWN error.message issue is appropriately tracked separately in CAMEL-24512.

I have no remaining blocking findings. Thanks for the thorough follow-up.

Generated by Codex on behalf of Luigi De Masi. This response was generated by an AI agent and may contain inaccuracies.

@oscerd
oscerd merged commit fcd9266 into apache:main Aug 27, 2026
5 checks passed
@oscerd
oscerd deleted the fix/CAMEL-24499 branch August 27, 2026 04:41
davsclaus pushed a commit that referenced this pull request Aug 28, 2026
…-exception filter (#1908)

RouteEndpointInfo annotates its properties map with
@JsonIgnoreProperties({"route.start.exception"}), but RouteDetailsEndpointInfo
re-declared the same field without the annotation plus a getProperties()
override, so the actuator detail operation serialized the property the base view
filters. That property holds the Throwable itself, so Jackson emitted the whole
object graph - nested causes, every stack frame with class, file and line,
classloader names and the JDK version.

Both the annotation and the shadowing field were introduced together under
CAMEL-20993, so the filter was intended and the subclass field simply escaped
it. The shadow and its getter are removed and the annotated base-class property
is used instead. getProperties() stays available on the subclass by
inheritance, so this is source and binary compatible.

The regression test uses Awaitility to wait until the supervising route
controller has actually recorded route.start.exception before building either
view, so the assertions cannot pass vacuously.

Closes #1898
davsclaus pushed a commit that referenced this pull request Aug 28, 2026
…-exception filter (#1909)

RouteEndpointInfo annotates its properties map with
@JsonIgnoreProperties({"route.start.exception"}), but RouteDetailsEndpointInfo
re-declared the same field without the annotation plus a getProperties()
override, so the actuator detail operation serialized the property the base view
filters. That property holds the Throwable itself, so Jackson emitted the whole
object graph - nested causes, every stack frame with class, file and line,
classloader names and the JDK version.

Both the annotation and the shadowing field were introduced together under
CAMEL-20993, so the filter was intended and the subclass field simply escaped
it. The shadow and its getter are removed and the annotated base-class property
is used instead. getProperties() stays available on the subclass by
inheritance, so this is source and binary compatible.

The regression test uses Awaitility to wait until the supervising route
controller has actually recorded route.start.exception before building either
view, so the assertions cannot pass vacuously.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants