camel-platform-http-starter: code review fixes (cookies, method restrict, exchange lifecycle, executors, ByteBuffer, unregister, SB4 idioms)#1842
Conversation
…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>
davsclaus
left a comment
There was a problem hiding this comment.
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
-
Cookie handler contract — Verified against
CookieHandlerinterface javadoc (nullfor missing, not"Cookie not found"). Matches the Vert.x implementation.removeCookiereads fromrequest.getCookies()(incoming), so reading after adding the expiry cookie to the response is safe. Removal of@CookieValueannotation is correct — it's a Spring MVC handler annotation, not appropriate here. -
Method restrict —
model.getVerbs()was used instead of the localverbsvariable (latent NPE). Untrimmed verbs ("GET, POST") produced null fromRequestMethod.resolve(" POST"), silently widening to all methods. Fail-closed withIllegalArgumentExceptionis the right behavior. -
Exchange lifecycle —
createExchange(true)+ manualreleaseExchangedouble-releases pooled exchanges. Changed tocreateExchange(false), consistent with bothVertxPlatformHttpConsumerandCamelServletin camel-core. The try/catch around setup +createUoWensures the exchange is released on failure. -
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 withshutdownExecutorflag. -
ByteBuffer response —
bb.array()leaked adjacent bytes for sliced buffers and threw for direct buffers. ThewriteByteBufferhelper correctly handles both heap and direct buffers. ThedoWriteDirectResponsecleanup replaces what appears to be decompiled code with readable source aligned toDefaultHttpBinding. -
Unregister mappings —
unregisterHttpEndpointwas a noop. Per-consumer tracking withConcurrentHashMapensures stopping one route doesn't affect other consumers on the same path with different methods. -
Spring Boot 4 idioms — Type-safe
after =references,Threading.VIRTUAL.isActive(),ServerProperties, correct@AutoConfigurationusage.
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.
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:CookieHandlercontract inSpringBootCookieHandler—getCookieValuereturned the sentinel string"Cookie not found"andremoveCookiereturned the cookie name; theCookieHandlercontract (and the Vert.x implementation) return the value, ornullwhen the cookie does not exist.httpMethodRestrictto all methods on unparsable verbs —httpMethodRestrict=GET, POST(note the space) silently registered a mapping serving all HTTP methods because the unresolved token became a nullRequestMethod. Tokens are now trimmed/upper-cased and an unknown verb fails closed. Also fixes a latent NPE (model.getVerbs()vs the localverbsvariable).createExchange(true)combined with the manualreleaseExchange(exchange, false)inafterProcessdouble-releases pooled exchanges (camel.main.exchange-factory=pooled); aligned withCamelServlet/VertxPlatformHttpConsumer(createExchange(false)+ manual release). Also releases the exchange when request parsing orcreateUoWfails.ExecutorServicethat 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.bb.array()dumped the whole backing array (sliced/partially-consumed buffers leaked adjacent bytes; direct buffers threw). Also replaces the visibly decompileddoWriteDirectResponsebody (var19locals) with readable source re-aligned toDefaultHttpBinding.unregisterHttpEndpointwas 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.@AutoConfigurationfor the imports-file-registered WebMvc configuration,Threading.VIRTUAL.isActive()instead of manual property parsing,ServerPropertiesinstead of rawserver.portparsing, type-safeafter =references, no re-enabling of Boot-ownedWebMvcProperties, specific exceptions. Executor selection semantics are unchanged.Every fix comes with tests (new test classes:
SpringBootPlatformHttpMethodRestrictTest,SpringBootPlatformHttpPooledExchangeTest,SpringBootPlatformHttpConsumerExecutorTest,SpringBootPlatformHttpByteBufferResponseTest,SpringBootPlatformHttpRouteLifecycleTest, plus extended cookie tests). Fullmvn verifyon the module passes.Target
Claude Code on behalf of Federico Mariani (@Croway)
🤖 Generated with Claude Code