Skip to content

camel-platform-http-starter: code review fixes (cookies, method restrict, exchange lifecycle, executors, ByteBuffer, unregister, SB4 idioms)#1842

Merged
Croway merged 7 commits into
apache:mainfrom
Croway:platform-http-starter-code-review-fixes
Jul 10, 2026
Merged

camel-platform-http-starter: code review fixes (cookies, method restrict, exchange lifecycle, executors, ByteBuffer, unregister, SB4 idioms)#1842
Croway merged 7 commits into
apache:mainfrom
Croway:platform-http-starter-code-review-fixes

Conversation

@Croway

@Croway Croway commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Description

This PR addresses the findings of a code review of camel-platform-http-starter. Each finding is an isolated commit so they can be reviewed (or reverted) independently:

  1. Honor CookieHandler contract in SpringBootCookieHandlergetCookieValue returned the sentinel string "Cookie not found" and removeCookie returned the cookie name; the CookieHandler contract (and the Vert.x implementation) return the value, or null when the cookie does not exist.
  2. Do not widen httpMethodRestrict to all methods on unparsable verbshttpMethodRestrict=GET, POST (note the space) silently registered a mapping serving all HTTP methods because the unresolved token became a null RequestMethod. Tokens are now trimmed/upper-cased and an unknown verb fails closed. Also fixes a latent NPE (model.getVerbs() vs the local verbs variable).
  3. Fix consumer exchange lifecyclecreateExchange(true) combined with the manual releaseExchange(exchange, false) in afterProcess double-releases pooled exchanges (camel.main.exchange-factory=pooled); aligned with CamelServlet/VertxPlatformHttpConsumer (createExchange(false) + manual release). Also releases the exchange when request parsing or createUoW fails.
  4. Fix consumer executor ownership — every consumer allocated a throwaway single-thread ExecutorService that was immediately overwritten; when the fallback executor was used it was never shut down; an engine created via the port-only constructor produced consumers that NPE'd on the first request.
  5. Write only the ByteBuffer window in direct responsesbb.array() dumped the whole backing array (sliced/partially-consumed buffers leaked adjacent bytes; direct buffers threw). Also replaces the visibly decompiled doWriteDirectResponse body (var19 locals) with readable source re-aligned to DefaultHttpBinding.
  6. Unregister Spring MVC mappings when a route stopsunregisterHttpEndpoint was a noop, so stopped/removed routes kept dispatching to a stopped consumer instead of returning 404. Mappings are now tracked per consumer, which keeps same-path/different-method consumers independent and sidesteps the shared-OPTIONS-handler issue noted in the code.
  7. Align auto-configuration with Spring Boot 4 idioms@AutoConfiguration for the imports-file-registered WebMvc configuration, Threading.VIRTUAL.isActive() instead of manual property parsing, ServerProperties instead of raw server.port parsing, type-safe after = references, no re-enabling of Boot-owned WebMvcProperties, specific exceptions. Executor selection semantics are unchanged.

Every fix comes with tests (new test classes: SpringBootPlatformHttpMethodRestrictTest, SpringBootPlatformHttpPooledExchangeTest, SpringBootPlatformHttpConsumerExecutorTest, SpringBootPlatformHttpByteBufferResponseTest, SpringBootPlatformHttpRouteLifecycleTest, plus extended cookie tests). Full mvn verify on the module passes.

Target

  • main
  • backport needed

Claude Code on behalf of Federico Mariani (@Croway)

🤖 Generated with Claude Code

Croway and others added 7 commits July 10, 2026 17:08
…otCookieHandler

getCookieValue returned the sentinel string "Cookie not found" and
removeCookie returned the cookie name, while the CookieHandler contract
(and the Vert.x implementation) return the cookie value or null when the
cookie does not exist. Both now return the actual value or null.

Also remove the stray @CookieValue annotation (this is not an annotated
controller method) and read the endpoint CookieConfiguration once in the
constructor instead of re-assigning a shared field on every request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ethods on unparsable verbs

asRequestMappingInfo split model.getVerbs() instead of the local verbs
variable (a latent NPE when the verbs are resolved from
httpMethodRestrict) and passed unresolved verbs as null RequestMethods.
A null method caused createRequestMappingInfo to register the mapping
without any method restriction, so a value like 'GET, POST' (whitespace
after the comma) silently served ALL HTTP methods.

Verb tokens are now trimmed and upper-cased before resolving, and an
unresolvable verb fails closed with IllegalArgumentException (the
endpoint is then not registered and a warning is logged) instead of
failing open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The consumer created the exchange with createExchange(true) (auto
release via UnitOfWork) but afterProcess also manually calls
releaseExchange(exchange, false), which force-dones the exchange a
second time when the pooled exchange factory is configured - the same
exchange could be handed out to two concurrent requests. Other HTTP
consumers (CamelServlet, VertxPlatformHttpConsumer) use
createExchange(false) with a manual release; do the same here.

Additionally, when reading the request (PlatformHttpMessage.init) or
createUoW failed, the exchange was never released at all. The setup is
now guarded so the exchange is released on failure before rethrowing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three-arg consumer constructor chained through the two-arg one, so
every consumer allocated a throwaway single-thread ExecutorService that
was immediately overwritten. When the two-arg constructor executor WAS
used, it was never shut down when the consumer stopped, leaking its
worker thread.

The consumer now only creates its own executor when none is provided,
shuts it down in doStop (a provided executor is left untouched, it is
owned by Spring), and rejects a null executor up front. An engine
created with the port-only constructor no longer produces consumers
that NPE on the first request - its consumers manage their own
executor instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ct responses

doWriteDirectResponse wrote bb.array(), which dumps the entire backing
array of a ByteBuffer body: a sliced or partially consumed buffer
leaked bytes outside its position/limit window, and a direct or
read-only buffer (no accessible backing array) threw and produced a
500. The buffer's remaining window is now written via
array/arrayOffset/position when a backing array is available, copying
through a duplicate otherwise.

The method body was visibly decompiled code (var19 locals, redundant
casts) drifted from DefaultHttpBinding.doWriteDirectResponse it was
copied from; it is re-aligned with the parent as readable source, using
the Exchange.CONTENT_TYPE and HttpConstants constants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ute stops

unregisterHttpEndpoint was a noop, so stopping or removing a route left
its mapping registered and requests kept being dispatched to a stopped
consumer instead of returning 404.

The mapping now tracks the RequestMappingInfos it registered per
consumer and removes exactly those when the endpoint is unregistered.
Tracking per consumer (not per uri) keeps consumers serving the same
path with different methods independent, and sidesteps the shared
OPTIONS handler problem described in asRequestMappingInfo since only
mappings owned by the stopped consumer are touched. Restarting the
route registers the mappings again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t 4 idioms

- SpringBootPlatformWebMvcConfiguration is registered in
  AutoConfiguration.imports, so annotate it @autoConfiguration (which
  also implies proxyBeanMethods=false) instead of plain @configuration
- detect virtual threads with Threading.VIRTUAL.isActive() instead of
  manually parsing spring.threads.virtual.enabled
- resolve the server port from ServerProperties instead of parsing the
  raw server.port property
- use type-safe after= references instead of afterName strings
- do not re-enable Spring Boot's own WebMvcProperties via
  @EnableConfigurationProperties; the owning auto-configuration does
- throw IllegalStateException instead of bare RuntimeException when no
  suitable executor is found, drop the unused ObjectProvider import

Executor selection semantics are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Croway Croway requested review from davsclaus and gzurowski July 10, 2026 15:43

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

Review Summary

All 7 fixes are correct, well-motivated, and backed by appropriate tests. Each fix is isolated in a separate commit, making review and potential reversion straightforward. CI passes.

Verified Correctness

  1. Cookie handler contract — Verified against CookieHandler interface javadoc (null for missing, not "Cookie not found"). Matches the Vert.x implementation. removeCookie reads from request.getCookies() (incoming), so reading after adding the expiry cookie to the response is safe. Removal of @CookieValue annotation is correct — it's a Spring MVC handler annotation, not appropriate here.

  2. Method restrictmodel.getVerbs() was used instead of the local verbs variable (latent NPE). Untrimmed verbs ("GET, POST") produced null from RequestMethod.resolve(" POST"), silently widening to all methods. Fail-closed with IllegalArgumentException is the right behavior.

  3. Exchange lifecyclecreateExchange(true) + manual releaseExchange double-releases pooled exchanges. Changed to createExchange(false), consistent with both VertxPlatformHttpConsumer and CamelServlet in camel-core. The try/catch around setup + createUoW ensures the exchange is released on failure.

  4. Executor ownership — Old 3-arg constructor created a throwaway Executors.newSingleThreadExecutor() immediately overwritten. Port-only engine constructor produced consumers with null executor (NPE). Now properly tracked with shutdownExecutor flag.

  5. ByteBuffer responsebb.array() leaked adjacent bytes for sliced buffers and threw for direct buffers. The writeByteBuffer helper correctly handles both heap and direct buffers. The doWriteDirectResponse cleanup replaces what appears to be decompiled code with readable source aligned to DefaultHttpBinding.

  6. Unregister mappingsunregisterHttpEndpoint was a noop. Per-consumer tracking with ConcurrentHashMap ensures stopping one route doesn't affect other consumers on the same path with different methods.

  7. Spring Boot 4 idioms — Type-safe after = references, Threading.VIRTUAL.isActive(), ServerProperties, correct @AutoConfiguration usage.

Minor Observation (non-blocking)

In SpringBootPlatformHttpConsumer.doStop(), when shutdownExecutor is true (consumer-owned executor), the executor is shut down. If the route is later restarted (not just the context stopped), the same consumer would have a dead executor. This only affects the fallback path (SpringBootPlatformHttpEngine(port) without an executor), not the normal auto-configuration path. The current code on main never shuts down the executor (thread leak), so this PR is strictly better. Could be addressed as a follow-up if needed.

Test Coverage

5 new test classes plus 4 extended cookie tests cover all fixed scenarios including concurrent pooled exchanges, sliced/direct ByteBuffers, route stop/restart, and shared-path independence.


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

@Croway Croway merged commit 595c737 into apache:main Jul 10, 2026
5 checks passed
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.

2 participants