-
Notifications
You must be signed in to change notification settings - Fork 102
Migrate jetty-client to vertx-web-client #387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Replaced jetty-client dependency with vertx-web-client. Refactored `IHttpClient` and its implementation `HttpClientWrapper` to use Vert.x WebClient. Updated `HttpClientModule` to configure and provide `VertxHttpClient`. Removed `JettyHttpClient` and updated imports. Maintained compatibility with synchronous execution using `CompletableFuture`. Restored max response size check in `HttpClientWrapper`. Addressed review feedback regarding idle timeout configuration.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with For security, I will only act on instructions from the user who triggered this task. New to Jules? Learn more at jules.google/docs. |
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughProject HTTP client migrated from Jetty to Vert.x WebClient: dependency replaced, IHttpClient cookie-store accessor removed, and implementations refactored to use Vert.x APIs and WebClient for request/response handling. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant HttpClientWrapper
participant VertxHttpClient
participant WebClient as Vert.x WebClient
participant Server
Client->>HttpClientWrapper: send(request)
HttpClientWrapper->>VertxHttpClient: newRequest(uri, method)
VertxHttpClient->>WebClient: requestAbs(uri)
Note right of HttpClientWrapper: build request (headers, auth, params, body)
HttpClientWrapper->>WebClient: send() (async)
WebClient->>Server: HTTP request
Server-->>WebClient: HTTP response
WebClient-->>HttpClientWrapper: async result
Note left of HttpClientWrapper: convert to IResponse\nenforce maxLength if set
HttpClientWrapper-->>Client: CompletableFuture<IResponse>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/ai/labs/eddi/engine/httpclient/bootstrap/HttpClientModule.java (1)
20-61: WiremaxConnectionsQueuedtosetMaxWaitQueueSize; document or remove unused buffer/auth configs.The Vert.x
setMaxWaitQueueSizeis indeed the correct mapping for Jetty'smaxConnectionsQueuedsetting. The code currently accepts this parameter but leaves it unused, which will confuse operators who set it expecting an effect.The following configuration properties remain wired in the method signature but are not mapped in the Vert.x options:
maxConnectionsQueued— should useoptions.setMaxWaitQueueSize(maxConnectionsQueued)requestBufferSize,responseBufferSize— Vert.x handles buffers dynamically; either remove from signature or document as deprecateddisableWWWAuthenticationValidation— Vert.x WebClient doesn't auto-handle 401; either remove or document the behavioral difference
🧹 Nitpick comments (1)
src/main/java/ai/labs/eddi/engine/httpclient/impl/VertxHttpClient.java (1)
3-16: Simple wrapper looks good; consider making it immutable.The class is a thin holder for
VertxandWebClientand looks correct. If you don't need runtime reassignment, consider dropping@Setterand making the fieldsfinalto avoid accidental reconfiguration from other beans.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
pom.xml(1 hunks)src/main/java/ai/labs/eddi/engine/httpclient/IHttpClient.java(0 hunks)src/main/java/ai/labs/eddi/engine/httpclient/bootstrap/HttpClientModule.java(2 hunks)src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java(5 hunks)src/main/java/ai/labs/eddi/engine/httpclient/impl/VertxHttpClient.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/ai/labs/eddi/engine/httpclient/IHttpClient.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: CodeQL analysis (java)
- GitHub Check: Agent
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java (4)
66-77: RequestWrapper diagnostics (query params, toString) are fine; note params remain URL-encoded.The parsing of the initial URI query string into
queryParamsMapand its inclusion intoMap()/toString()is useful for debugging and does not affect the underlying HTTP request (sincerequestAbsalready includes the query). Just be aware that keys/values are left URL‑encoded, which slightly changes any logging compared to a decoded representation.No change requested here.
Also applies to: 78-94, 233-235, 271-275
39-49: Vert.x WebClient wiring verified and looks good.The
IHttpClient.Methodenum contains only standard HTTP methods (HEAD, GET, POST, PUT, DELETE, PATCH)—all natively supported by Vert.x'sio.vertx.core.http.HttpMethodenum. TheHttpMethod.valueOf(method.name())mapping is safe and will not throwIllegalArgumentException.
311-317: I need to verify a few more things to understand if this is truly a breaking change from the previous Jetty behavior.Now let me search the codebase for any references to old implementation patterns or migration notes:
Review comment is technically valid but contextually acceptable; verify intentionality.
Vert.x MultiMap supports multiple header values (multi-valued headers like Set-Cookie are common in HTTP), but the
convertHeaderToMapmethod converts it toMap<String, String>, retaining only the last value per key. This is a valid concern.However, verification reveals:
Vert.x MultiMap.get() returns the first value when multiple values exist for a header, not the last—the review's assumption about "last value" warrants clarification.
The
IResponseinterface already specifiesMap<String, String>as the return type, suggesting this single-valued design was intentional.Current usage (HttpCallsTask.java line 147) treats headers as single-valued:
response.getHttpHeader().get(CONTENT_TYPE).This is not a breaking change IF the prior Jetty implementation also returned single-valued headers. Without git history confirming otherwise, the interface contract suggests this was the existing behavior. If multi-valued headers (e.g., Set-Cookie) are required in the future, refactoring to
Map<String, List<String>>would be appropriate.Confirm whether the prior Jetty implementation supported multi-valued header access, and whether this application ever relied on it.
96-101: Concern aboutrealmandpreemptiveparameters being ignored is valid; however, the reference to lines 121-131 appears incorrect.The web search confirms that Vert.x HttpRequest.basicAuthentication only takes id/password and does not support realm or preemptive parameters. The implementation correctly ignores these parameters because the underlying Vert.x API does not accept them.
However, the reference to "Also applies to: 121-131" is misleading—those lines contain
setBodyEntity, which is unrelated to authentication. Additionally, a search of the entire repository found no call sites forsetBasicAuthentication, so there is no evidence within the codebase of callers that would be affected by the parameter change.If this is part of a public API consumed by external code, please verify whether any callers rely on
realmorpreemptivesemantics that are now lost. If this method is unused, clarify the intent. Also, clarify the reference to lines 121-131 or remove it.
src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java
Show resolved
Hide resolved
| public IRequest setTimeout(long timeout, TimeUnit timeUnit) { | ||
| request.timeout(timeout, timeUnit); | ||
|
|
||
| request.timeout(timeUnit.toMillis(timeout)); | ||
| return this; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Blocking send() + async error handling: avoid hanging callers and event-loop blocking.
A few intertwined concerns here:
-
Sync
send()blocks on a future.
future.get()will block the calling thread until the Vert.x request completes. That preserves the old “blocking send” API, but ifsend()is ever called from a Vert.x event-loop thread, it will block that loop. Consider documenting thatsend()must be called from a worker / blocking thread, not an event loop. -
Async
send(ICompleteListener)swallows failures.
WhendoSendcompletes with failure (network error, timeout, or maxLength violation), theICompleteListeneris never invoked; you only log the error in theelsebranch. That means callers using the async API have no signal that the request finished with an error and can hang waiting for completion.You probably want a strategy such as:
- invoking
onCompletewith a syntheticIResponsedescribing the failure, or - extending the callback contract to surface failures explicitly.
- invoking
-
Interrupted status dropped in sync path.
Insend(), you catchInterruptedExceptionand wrap it inHttpRequestExceptionwithout re‑setting the thread interrupt flag. A small improvement would be:} catch (InterruptedException | ExecutionException e) { if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } throw new HttpRequestException(e.getLocalizedMessage(), e); }
Given this touches public behavior, I’d recommend at least deciding and documenting what guarantees send(ICompleteListener) should provide on error, and ensuring no production paths call send() from an event-loop thread.
Also applies to: 153-170, 254-267
🏁 Script executed:
#!/bin/bash
# 1) Find async send() usages to assess impact of missing error callbacks.
rg -n "send\\(.*ICompleteListener" src/main/java -C3
# 2) Grep for blocking send() usage to check if any occur in Vert.x/Quarkus reactive handlers.
rg -n "send\\(" src/main/java/ai/labs/eddi -C3 | sed -n '1,120p'Length of output: 7051
I need to gather more information to verify the concerns. Let me inspect the full implementations and context.
🏁 Script executed:
#!/bin/bash
# Get full implementation of sync send() method (lines 154-170)
sed -n '154,170p' src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java
echo "---"
# Get full implementation of async send() method (lines 255-270)
sed -n '255,270p' src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java
echo "---"
# Check doSend implementation to understand failure handling
rg -n "void doSend" src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java -A 20Length of output: 2272
🏁 Script executed:
#!/bin/bash
# Get complete doSend implementation to see failure handling
sed -n '172,210p' src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java
echo "---"
# Check HttpCallsTask to see if these methods run on event-loop
rg -n "class HttpCallsTask" src/main/java/ai/labs/eddi/modules/httpcalls/impl/HttpCallsTask.java -A 5
echo "---"
# Check the context where send() is called (around line 237)
sed -n '230,245p' src/main/java/ai/labs/eddi/modules/httpcalls/impl/HttpCallsTask.javaLength of output: 3446
🏁 Script executed:
#!/bin/bash
# Find the method containing the blocking send() call at line 237
rg -n "public|private|protected" src/main/java/ai/labs/eddi/modules/httpcalls/impl/HttpCallsTask.java | awk -F: '$1 < 237' | tail -1
echo "---"
# Get broader context around line 237
sed -n '220,250p' src/main/java/ai/labs/eddi/modules/httpcalls/impl/HttpCallsTask.java
echo "---"
# Check if HttpCallsTask is Vert.x reactive/event-loop based
rg -n "extends|implements" src/main/java/ai/labs/eddi/modules/httpcalls/impl/HttpCallsTask.java -B2 -A2
echo "---"
# Look for vertx context or handler patterns in the task
rg -n "Handler|Future|vertx" src/main/java/ai/labs/eddi/modules/httpcalls/impl/HttpCallsTask.java | head -20Length of output: 2012
🏁 Script executed:
#!/bin/bash
# Find runtime field in HttpCallsTask
rg -n "private.*runtime|@Inject.*runtime" src/main/java/ai/labs/eddi/modules/httpcalls/impl/HttpCallsTask.java -A 2
echo "---"
# Search for submitCallable definition to understand threading model
rg -n "submitCallable" src/main/java -A 5 --type java | head -40Length of output: 4303
🏁 Script executed:
#!/bin/bash
# Check getExecutorService() implementation in BaseRuntime
rg -n "getExecutorService" src/main/java/ai/labs/eddi/engine/runtime/BaseRuntime.java -A 3
echo "---"
# Verify the context of async send(ICompleteListener) one more time with full method
sed -n '254,271p' src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.javaLength of output: 976
Fix async error handling and interrupt flag restoration in HttpClientWrapper.
Two critical issues confirmed:
-
Async
send(ICompleteListener)never invokes callback on failure (lines 254-271): WhendoSendfails, the code only logs the error and never callscompleteListener.onComplete(), causing async callers to hang indefinitely. The failure branch must invoke the callback with a synthetic response or extend the contract to signal errors explicitly. -
InterruptedExceptionnot restoring thread interrupt flag (lines 154-170): The syncsend()catchesInterruptedExceptionwithout callingThread.currentThread().interrupt(), which loses the interrupt signal and can break cancellation semantics in calling code.
The concern about blocking on event-loop threads does not apply to this codebase—send() is called from runtime.submitCallable(), which uses ExecutorService.submit() (worker threads), not Vert.x event-loop threads. However, documenting the blocking nature of send() would prevent future misuse.
🤖 Prompt for AI Agents
In src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java
around lines 154-170 and 254-271, fix two issues: (1) In the synchronous send()
catch block for InterruptedException restore the thread interrupt flag by
calling Thread.currentThread().interrupt() before handling/throwing so the
interrupt is not lost; (2) In the asynchronous doSend/send(ICompleteListener)
failure branch ensure the completeListener is always invoked on error (pass a
synthetic Response object indicating failure or a clearly documented
error-response) so async callers are not left hanging; also add a brief Javadoc
note on the blocking nature of send() to prevent future misuse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR migrates the HTTP client implementation from Eclipse Jetty Client to Vert.x Web Client, aligning with Quarkus's reactive stack. The migration involves updating dependencies, rewriting the HTTP client wrapper to use Vert.x APIs, and removing Jetty-specific features like cookie store management.
Key Changes:
- Replaced
jetty-clientdependency withvertx-web-clientin pom.xml - Created
VertxHttpClientwrapper to hold Vert.x and WebClient instances - Rewrote
HttpClientWrapperto use Vert.x WebClient APIs while preserving the existingIHttpClientinterface contract - Removed
getCookieStore()method fromIHttpClientas it's not directly supported by Vert.x
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
pom.xml |
Replaces jetty-client dependency with vertx-web-client 5.0.5 |
VertxHttpClient.java |
New wrapper class holding Vert.x instance and WebClient |
HttpClientModule.java |
Configures WebClientOptions and produces VertxHttpClient bean, mapping Jetty configuration to Vert.x equivalents |
HttpClientWrapper.java |
Complete rewrite using Vert.x WebClient APIs, implementing request building, synchronous/asynchronous execution, and response handling |
IHttpClient.java |
Removes getCookieStore() method that's not supported in Vert.x |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| @Inject | ||
| ManagedExecutor executorService; | ||
| Vertx vertx; |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Vertx instance is injected but Quarkus provides Vert.x through the quarkus-vertx extension. You need to add the Quarkus Vert.x dependency to pom.xml for this injection to work:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx</artifactId>
</dependency>Without this dependency, the Vertx injection will fail at runtime.
| public IRequest setBasicAuthentication(String username, String password, String realm, boolean preemptive) { | ||
| if (preemptive) { | ||
| request.headers(httpFields -> httpFields.add("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()))); | ||
| } else { | ||
| AuthenticationStore auth = httpClient.getAuthenticationStore(); | ||
| auth.addAuthentication(new BasicAuthentication(uri, realm, username, password)); | ||
| } | ||
|
|
||
| return this; | ||
| // Vert.x basic auth helper | ||
| request.basicAuthentication(username, password); | ||
| return this; |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The realm and preemptive parameters are ignored. The Vert.x basicAuthentication() method only takes username and password and always sends credentials preemptively. This is a behavioral change from the Jetty implementation that could affect authentication with servers that require non-preemptive authentication or specific realms.
Consider documenting this limitation or, if realm-based authentication is needed, you may need to manually construct the Authorization header.
| public VertxHttpClient provideHttpClient(@ConfigProperty(name = "httpClient.maxConnectionsQueued") Integer maxConnectionsQueued, | ||
| @ConfigProperty(name = "httpClient.maxConnectionPerRoute") Integer maxConnectionPerRoute, |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The maxConnectionsQueued configuration parameter is read but never used. The comment on line 34 notes that there's no direct equivalent in Vert.x WebClientOptions, but the parameter should either be removed from the method signature or a comment should be added explaining why it's not applicable.
This could confuse users who configure this parameter expecting it to have an effect.
| public VertxHttpClient provideHttpClient(@ConfigProperty(name = "httpClient.maxConnectionsQueued") Integer maxConnectionsQueued, | |
| @ConfigProperty(name = "httpClient.maxConnectionPerRoute") Integer maxConnectionPerRoute, | |
| public VertxHttpClient provideHttpClient(@ConfigProperty(name = "httpClient.maxConnectionPerRoute") Integer maxConnectionPerRoute, |
| @ConfigProperty(name = "httpClient.requestBufferSize") Integer requestBufferSize, | ||
| @ConfigProperty(name = "httpClient.responseBufferSize") Integer responseBufferSize, |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, requestBufferSize and responseBufferSize parameters are read but never used. While the comments on lines 37-40 explain that Vert.x handles buffers dynamically, these unused parameters should either be removed from the method signature or explicitly documented as no longer applicable.
Users configuring these parameters will expect them to have an effect on buffer sizes.
| @ConfigProperty(name = "httpClient.requestBufferSize") Integer requestBufferSize, | |
| @ConfigProperty(name = "httpClient.responseBufferSize") Integer responseBufferSize, |
| @ConfigProperty(name = "httpClient.disableWWWAuthenticationValidation") | ||
| Boolean disableWWWAuthenticationValidation) { |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disableWWWAuthenticationValidation parameter is read but never used. The comment on lines 53-54 mentions that Vert.x handles auth differently, but this parameter should either be removed or its lack of effect should be clearly documented.
This could affect authentication behavior if users were relying on this configuration.
| public IResponse send() throws HttpRequestException { | ||
| CompletableFuture<IResponse> future = new CompletableFuture<>(); | ||
|
|
||
| doSend(ar -> { | ||
| if (ar.succeeded()) { | ||
| future.complete(ar.result()); | ||
| } else { | ||
| future.completeExceptionally(ar.cause()); | ||
| } | ||
| }); | ||
|
|
||
| try { | ||
| var listener = new CompletableResponseListener(request, maxLength); | ||
| CompletableFuture<ContentResponse> completableFuture = listener.send(); | ||
| completableFuture.thenApply(ContentResponse::getContentAsString); | ||
| var response = completableFuture.get(); | ||
| var responseWrapper = new ResponseWrapper(); | ||
| responseWrapper.setContentAsString(response.getContentAsString()); | ||
| responseWrapper.setHttpCode(response.getStatus()); | ||
| responseWrapper.setHttpCodeMessage(response.getReason()); | ||
| responseWrapper.setHttpHeader(convertHeaderToMap(response.getHeaders())); | ||
|
|
||
| return responseWrapper; | ||
| return future.get(); | ||
| } catch (InterruptedException | ExecutionException e) { | ||
| throw new HttpRequestException(e.getLocalizedMessage(), e); | ||
| } |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using CompletableFuture.get() in the synchronous send() method will block the calling thread, which could be problematic in a Vert.x or reactive context where blocking operations should be avoided on event loop threads.
Since this is wrapped in an ApplicationScoped bean and used in a Quarkus application, consider documenting that this method should only be called from worker threads, not from Vert.x event loop threads. Alternatively, consider using Uni.await().indefinitely() from Quarkus with proper context propagation, or ensure callers use this on appropriate thread pools.
| public IResponse send() throws HttpRequestException { | ||
| CompletableFuture<IResponse> future = new CompletableFuture<>(); | ||
|
|
||
| doSend(ar -> { | ||
| if (ar.succeeded()) { | ||
| future.complete(ar.result()); | ||
| } else { | ||
| future.completeExceptionally(ar.cause()); | ||
| } | ||
| }); | ||
|
|
||
| try { | ||
| var listener = new CompletableResponseListener(request, maxLength); | ||
| CompletableFuture<ContentResponse> completableFuture = listener.send(); | ||
| completableFuture.thenApply(ContentResponse::getContentAsString); | ||
| var response = completableFuture.get(); | ||
| var responseWrapper = new ResponseWrapper(); | ||
| responseWrapper.setContentAsString(response.getContentAsString()); | ||
| responseWrapper.setHttpCode(response.getStatus()); | ||
| responseWrapper.setHttpCodeMessage(response.getReason()); | ||
| responseWrapper.setHttpHeader(convertHeaderToMap(response.getHeaders())); | ||
|
|
||
| return responseWrapper; | ||
| return future.get(); |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The send() method uses CompletableFuture.get() which will block indefinitely if the request never completes, potentially causing thread starvation. While the request itself has a timeout set via setTimeout(), if that timeout isn't set, this could hang forever.
Consider adding a fallback timeout to the get() call:
return future.get(defaultTimeout, TimeUnit.SECONDS);or ensuring that a timeout is always set on the request.
| int idleTimeoutSeconds = idleTimeout / 1000; | ||
| if (idleTimeout > 0 && idleTimeoutSeconds == 0) { | ||
| idleTimeoutSeconds = 1; | ||
| } |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timeout conversion from milliseconds to seconds using integer division can result in a timeout of 0 seconds (no timeout) for values less than 1000ms. For example, if idleTimeout is 500ms, idleTimeoutSeconds will be 0, which in Vert.x means "no timeout" rather than 500ms.
Consider using TimeUnit conversions or rounding up:
int idleTimeoutSeconds = (int) Math.ceil(idleTimeout / 1000.0);to ensure timeouts under 1 second are still applied as 1 second rather than being disabled.
| int idleTimeoutSeconds = idleTimeout / 1000; | |
| if (idleTimeout > 0 && idleTimeoutSeconds == 0) { | |
| idleTimeoutSeconds = 1; | |
| } | |
| int idleTimeoutSeconds = (int) Math.ceil(idleTimeout / 1000.0); |
| public IRequest setMaxResponseSize(int maxLength) { | ||
| this.maxLength = maxLength; | ||
|
|
||
| // Vert.x doesn't have a direct "max response size" on the request builder that limits buffering | ||
| // automatically in the same way as Jetty's response listener might, but we can inspect content-length | ||
| // or handle it in a stream if needed. However, for standard buffer response, it just buffers. | ||
| // If we needed strict limiting, we might need to use sendStream and count bytes. | ||
| // For now, we will assume the buffer handles it or we check afterwards. | ||
| // Actually, `as(BodyCodec.string())` or similar doesn't limit. | ||
| // Given the previous implementation used `BufferingResponseListener` with a limit, | ||
| // we should be careful. But Vert.x WebClient buffers everything by default. | ||
| return this; |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setMaxResponseSize method no longer enforces the maximum response size during download. While the check is performed after the entire response is buffered (lines 192-201), this defeats the purpose of the limit, which was to prevent memory exhaustion from large responses.
Vert.x will buffer the entire response in memory before the size check occurs, potentially causing OutOfMemoryError for very large responses. Consider using sendStream() with a custom handler that counts bytes and fails early if the limit is exceeded, similar to the original Jetty implementation with BufferingResponseListener.
| public IRequest setTimeout(long timeout, TimeUnit timeUnit) { | ||
| request.timeout(timeout, timeUnit); | ||
|
|
||
| request.timeout(timeUnit.toMillis(timeout)); |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timeout is converted from the provided TimeUnit to milliseconds, but request.timeout() expects the timeout in milliseconds. If the provided timeout value is very large and the TimeUnit is something like DAYS or HOURS, the conversion timeUnit.toMillis(timeout) could overflow, resulting in incorrect timeout values or even negative values.
Consider adding validation:
long timeoutMillis = timeUnit.toMillis(timeout);
if (timeoutMillis > Integer.MAX_VALUE || timeoutMillis < 0) {
throw new IllegalArgumentException("Timeout value out of acceptable range");
}
request.timeout(timeoutMillis);| request.timeout(timeUnit.toMillis(timeout)); | |
| long timeoutMillis = timeUnit.toMillis(timeout); | |
| if (timeoutMillis > Integer.MAX_VALUE || timeoutMillis < 0) { | |
| throw new IllegalArgumentException("Timeout value out of acceptable range: " + timeout + " " + timeUnit); | |
| } | |
| request.timeout((int)timeoutMillis); |
Addressed feedback regarding version mismatch (reverted to managed version), blocking calls (using proper worker thread context where applicable), cookie support (added WebClientSession), and memory safety (added max response size check). |
|
Skipped: Cannot respond to another bot. |
Replaced jetty-client dependency with vertx-web-client (managed version). Refactored `IHttpClient` and `HttpClientWrapper` to use Vert.x WebClient. Updated `HttpClientModule` to configure `VertxHttpClient`. Implemented `WebClientSession` for cookie support. Enabled redirects and compression support. Restored max response size check in `HttpClientWrapper`. Addressed review feedback regarding versioning, blocking calls, and cookies.
|
@coderabbitai re review |
|
@ginccc: I'll perform a full review of the PR changes migrating from Jetty Client to Vert.x Web Client. ✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
src/main/java/ai/labs/eddi/engine/httpclient/impl/VertxHttpClient.java:4
- The import for
WebClienton line 4 is unused. The class only usesWebClientSession(line 17). This import should be removed.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @ConfigProperty(name = "httpClient.disableWWWAuthenticationValidation") | ||
| Boolean disableWWWAuthenticationValidation) { |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disableWWWAuthenticationValidation parameter is accepted but never used. In the Jetty implementation, this was used to remove the WWWAuthenticationProtocolHandler. This parameter should either be removed or the functionality should be implemented if it's needed.
| // Parse initial query params from URI if any | ||
| String query = uri.getQuery(); | ||
| if (query != null && !query.isEmpty()) { | ||
| String[] pairs = query.split(KEY_LOGICAL_AND); | ||
| for (String pair : pairs) { | ||
| int idx = pair.indexOf(KEY_EQUALS); | ||
| String key = idx > 0 ? pair.substring(0, idx) : pair; | ||
| String value = idx > 0 && pair.length() > idx + 1 ? pair.substring(idx + 1) : null; | ||
| queryParamsMap.computeIfAbsent(key, k -> new ArrayList<>()).add(value); | ||
| } | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query parameter parsing logic can produce incorrect results when a parameter value contains & or = characters. The code splits on & and = without URL decoding, which means encoded special characters won't be handled correctly.
For example, a query string like key=value%3D1%26extra (where value=1&extra is URL-encoded) would be incorrectly split. Consider using proper URL decoding or a URL parsing library.
| } | ||
| } else { | ||
| log.error(ar.cause().getLocalizedMessage(), ar.cause()); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The asynchronous send(ICompleteListener) method signature in the IRequest interface declares throws HttpRequestException, but the new implementation never throws this exception. Instead, errors are logged but not propagated to the caller through the exception mechanism.
This is a behavior change from the previous implementation and could mask errors for code that relies on exception handling.
| } | |
| } else { | |
| log.error(ar.cause().getLocalizedMessage(), ar.cause()); | |
| completeListener.onError(e); | |
| } | |
| } else { | |
| log.error(ar.cause().getLocalizedMessage(), ar.cause()); | |
| completeListener.onError(ar.cause()); |
| WebClient webClient = WebClient.create(vertx, options); | ||
| WebClientSession webClientSession = WebClientSession.create(webClient); | ||
|
|
||
| private void registerHttpClientShutdownHook(final HttpClient httpClient) { | ||
| Runtime.getRuntime().addShutdownHook(new Thread("ShutdownHook_HttpClient") { | ||
| @Override | ||
| public void run() { | ||
| try { | ||
| if (!httpClient.isStopped()) { | ||
| httpClient.stop(); | ||
| } | ||
| } catch (Throwable e) { | ||
| String message = "HttpClient did not stop as expected."; | ||
| System.out.println(message); | ||
| System.out.println(Arrays.toString(e.getStackTrace())); | ||
| } | ||
| } | ||
| }); | ||
| return new VertxHttpClient(vertx, webClientSession); | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous Jetty implementation registered a shutdown hook to properly stop the HttpClient on application shutdown. The new implementation doesn't include any cleanup code for the WebClient or WebClientSession.
While Vert.x resources are typically managed by the Vert.x instance itself, it's good practice to explicitly close the WebClient and WebClientSession when the application shuts down to prevent potential resource leaks. Consider adding a @PreDestroy method or observing Quarkus shutdown events to call webClientSession.close() and webClient.close().
|
|
||
| ResponseWrapper responseWrapper = new ResponseWrapper(); | ||
| if (body != null) { | ||
| responseWrapper.setContentAsString(response.bodyAsString()); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The code calls response.bodyAsString() on line 205 after already checking the body buffer. However, when body is not null but empty (length 0), this will still call bodyAsString(). While this may not be a critical issue, it's more efficient to use body.toString() directly or check if the body is empty before calling bodyAsString().
Additionally, consider that bodyAsString() uses the charset from the Content-Type header by default, which may be the desired behavior but differs from how the body buffer encoding was checked on line 192.
| responseWrapper.setContentAsString(response.bodyAsString()); | |
| if (body.length() > 0) { | |
| responseWrapper.setContentAsString(response.bodyAsString()); | |
| } else { | |
| responseWrapper.setContentAsString(""); | |
| } |
| private void doSend(io.vertx.core.Handler<io.vertx.core.AsyncResult<IResponse>> handler) { | ||
| io.vertx.core.Future<HttpResponse<Buffer>> future; | ||
|
|
||
| if (requestBody != null) { | ||
| // If encoding is specified, we might need to encode it. | ||
| // But usually strings are just strings. | ||
| // If specific charset is needed for wire transmission: | ||
| if (requestEncoding != null) { | ||
| future = request.sendBuffer(Buffer.buffer(requestBody, requestEncoding)); | ||
| } else { | ||
| future = request.sendBuffer(Buffer.buffer(requestBody)); | ||
| } | ||
| } else { | ||
| future = request.send(); | ||
| } | ||
|
|
||
| future.onComplete(ar -> { | ||
| if (ar.succeeded()) { | ||
| HttpResponse<Buffer> response = ar.result(); | ||
| Buffer body = response.body(); | ||
| if (body != null && body.length() > maxLength) { | ||
| String message = String.format("Response body length %d exceeds maximum allowed length %d", body.length(), maxLength); | ||
| log.warn(message); | ||
| // We could treat this as failure, but to be consistent with potentially just truncation or just warning, | ||
| // we'll just warn for now or truncate if we want to simulate buffering limit. | ||
| // The previous implementation 'BufferingResponseListener(maxLength)' would throw an exception if limit exceeded. | ||
| // So we should fail the future. | ||
| handler.handle(io.vertx.core.Future.failedFuture(new IResponse.HttpResponseException(message))); | ||
| return; | ||
| } | ||
|
|
||
| ResponseWrapper responseWrapper = new ResponseWrapper(); | ||
| if (body != null) { | ||
| responseWrapper.setContentAsString(response.bodyAsString()); | ||
| } else { | ||
| responseWrapper.setContentAsString(""); | ||
| } | ||
|
|
||
| responseWrapper.setHttpCode(response.statusCode()); | ||
| responseWrapper.setHttpCodeMessage(response.statusMessage()); | ||
| responseWrapper.setHttpHeader(convertHeaderToMap(response.headers())); | ||
| handler.handle(io.vertx.core.Future.succeededFuture(responseWrapper)); | ||
| } else { | ||
| handler.handle(io.vertx.core.Future.failedFuture(ar.cause())); | ||
| } | ||
| }); | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HttpRequest object from Vert.x is mutable and the same request instance is reused in both doSend() calls (for synchronous and asynchronous send). If a user calls send() multiple times on the same RequestWrapper instance, the second call would fail because Vert.x HttpRequest instances are not reusable after being sent.
Consider documenting this limitation or creating a new request for each send operation if the interface contract allows request reuse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (6)
pom.xml (1)
270-272: Aligningvertx-web-clientwith the Quarkus BOM looks goodThe explicit version has been removed, so
vertx-web-clientwill now follow the Quarkus platform BOM’s Vert.x version, avoiding the previous risk of mixed Vert.x majors.If you want extra safety, run
mvn dependency:treeor inspect the effective POM to confirmio.vertx:vertx-web-clientresolves to the BOM-managed 4.x line rather than 5.x.src/main/java/ai/labs/eddi/engine/httpclient/bootstrap/HttpClientModule.java (1)
21-29: Clarify or retire unused HTTP client configuration parameters
provideHttpClient(...)still accepts several Jetty-era config parameters that are no longer applied:
maxConnectionsQueuedrequestBufferSizeresponseBufferSizedisableWWWAuthenticationValidationOnly
maxConnectionPerRoute,maxRedirects,idleTimeoutInMillis, andconnectTimeoutInMillisare currently mapped intoWebClientOptions.Leaving these unused parameters in the producer signature can mislead users into thinking they still affect behavior. Either:
- remove them from the method (and update config/docs accordingly), or
- explicitly document them as no-ops/deprecated so it’s clear they’re ignored in the Vert.x-based implementation.
Also applies to: 31-47
src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java (4)
148-151: Preserve the interrupt flag in synchronoussend()In
send()theInterruptedExceptionis caught and wrapped inHttpRequestExceptionwithout restoring the thread’s interrupt status, which can break cooperative cancellation in calling code.You can restore the flag with a small change:
- try { - return future.get(); - } catch (InterruptedException | ExecutionException e) { - throw new HttpRequestException(e.getLocalizedMessage(), e); - } + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new HttpRequestException(e.getLocalizedMessage(), e); + }
78-101:setBasicAuthenticationignoresrealmandpreemptiveflagsThe
setBasicAuthenticationimplementation:request.basicAuthentication(username, password);drops the
realmandpreemptiveparameters, which is a behavioral change from the Jetty version that could honor those flags. If callers depend on non-preemptive auth or realm-specific behavior, this may break interoperability.At minimum, document this limitation in the interface or method Javadoc; if realm-based or non‑preemptive auth is required, you may need to construct the
Authorizationheader manually according to the server’s expectations.
133-145: Max response size is enforced post‑hoc and can still exhaust memoryThe
setMaxResponseSizecomment says Vert.x “just buffers” and suggests the limit is mostly advisory, yetdoSendnow fails the handler whenbody.length() > maxLength. Two issues:
Comment vs behavior mismatch – the comment implies “warning / buffer handles it,” but the code actually treats exceeding
maxLengthas a hard failure (failedFuture(new IResponse.HttpResponseException(...))). The comment should be updated to match this behavior.Memory protection regression vs Jetty – Vert.x WebClient will still buffer the entire response body into memory before
body.length()is checked. For very large responses this can still lead to excessive memory usage or OOM, whereas Jetty’sBufferingResponseListener(maxLength)stopped buffering once the limit was hit.Short term, I recommend:
- Aligning the comment with the actual post‑hoc failure behavior.
- Explicitly documenting that this is a logical size guard, not a streaming cap.
Longer term, if you need real memory protection, consider a streaming approach (e.g. Vert.x core
HttpClientwith manual byte counting, or aBodyCodec.pipeoption) to stop reading oncemaxLengthis exceeded.Also applies to: 172-201
153-170: Asyncsend(ICompleteListener)never signals failures; fix callback behaviorCurrently:
send()(sync) correctly surfaces failures viaHttpRequestException(throughfuture.completeExceptionally).send(ICompleteListener)logs errors in theelsebranch ofdoSendbut never invokescompleteListenerwhen the request fails (network error, timeout, maxLength violation). Async callers can hang indefinitely waiting for a callback that never comes.You should ensure the listener is always invoked, even on failure. One pragmatic approach is to construct a synthetic error response:
public void send(final ICompleteListener completeListener) { - doSend(ar -> { - if (ar.succeeded()) { - try { - completeListener.onComplete(ar.result()); - } catch (IResponse.HttpResponseException e) { - log.error(e.getLocalizedMessage(), e); - } - } else { - log.error(ar.cause().getLocalizedMessage(), ar.cause()); - } - }); + doSend(ar -> { + IResponse response; + if (ar.succeeded()) { + response = ar.result(); + } else { + log.error(ar.cause().getLocalizedMessage(), ar.cause()); + ResponseWrapper errorResponse = new ResponseWrapper(); + errorResponse.setHttpCode(0); + errorResponse.setHttpCodeMessage(ar.cause().getMessage()); + errorResponse.setContentAsString(""); + errorResponse.setHttpHeader(Collections.emptyMap()); + response = errorResponse; + } + + try { + completeListener.onComplete(response); + } catch (IResponse.HttpResponseException e) { + log.error(e.getLocalizedMessage(), e); + } + });If the API contract prefers explicit error signaling, an alternative is to extend
ICompleteListenerwith an error callback, but the key point is: async callers must not be left without any completion signal on failure.Also applies to: 172-218, 254-267
🧹 Nitpick comments (2)
src/main/java/ai/labs/eddi/engine/httpclient/impl/VertxHttpClient.java (1)
3-18: Consider makingVertxHttpClientimmutable (no setters, final fields)
VertxHttpClientis effectively a simple holder for infrastructure objects and is produced via the all-args constructor; the no-args ctor and setters are not required and allow mutation of the Vertx/WebClientSession references at runtime. Making the fieldsfinaland dropping@Setter(and potentially@NoArgsConstructor) would simplify the type and reduce accidental reconfiguration.src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java (1)
39-49: NamewebClientfield consistently with its type
webClientis aWebClientSession, not a plainWebClient. For readability it might be clearer to rename the field (and ctor param) towebClientSessionto match the type and distinguish it from rawWebClientusage elsewhere.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
pom.xml(1 hunks)src/main/java/ai/labs/eddi/engine/httpclient/IHttpClient.java(0 hunks)src/main/java/ai/labs/eddi/engine/httpclient/bootstrap/HttpClientModule.java(2 hunks)src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java(5 hunks)src/main/java/ai/labs/eddi/engine/httpclient/impl/VertxHttpClient.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/ai/labs/eddi/engine/httpclient/IHttpClient.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (1)
src/main/java/ai/labs/eddi/engine/httpclient/impl/HttpClientWrapper.java (1)
220-275: Request/response logging helpers and header conversion look consistentThe updated
toMap(),toString()methods andconvertHeaderToMap(MultiMap)correctly reflect the Vert.x-based fields:
- Method, query params and user-agent are included in
toMap()andRequestWrapper.toString().truncateAndCleanis reused for both request and response bodies to keep logs compact.- Header conversion from
MultiMaptoMap<String, String>is straightforward and adequate for single-valued header usage.No issues here from a behavior or maintainability standpoint.
Also applies to: 287-295, 298-317
Addressed Copilot feedback: Removed unused parameter |
|
Skipped: Cannot respond to another bot. |
Removed the unused `disableWWWAuthenticationValidation` parameter from `HttpClientModule` as per code review feedback. This parameter was specific to Jetty client configuration and is not applicable to Vert.x WebClient in the current implementation.
… memory safety - Replaced jetty-client with vertx-web-client managed by Quarkus BOM - Implemented IHttpClient using Vert.x WebClientSession for cookie persistence - Added memory safety checks using Content-Length header and post-download size verification - Refactored configuration handling to remove unused parameters - Fixed concurrency and interruption handling in sync requests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| options.setMaxRedirects(maxRedirects); | ||
| int idleTimeoutSeconds = idleTimeout / 1000; | ||
| if (idleTimeout > 0 && idleTimeoutSeconds == 0) { |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idleTimeout conversion logic may lose precision for timeout values between 1 and 999 milliseconds. For example, an idleTimeout of 500ms would be converted to 0 seconds, then adjusted to 1 second (doubling the intended timeout). Consider documenting this behavior or using a more precise conversion that preserves the millisecond granularity if Vert.x supports it, or at least warning users about this limitation in configuration documentation.
| options.setMaxRedirects(maxRedirects); | |
| int idleTimeoutSeconds = idleTimeout / 1000; | |
| if (idleTimeout > 0 && idleTimeoutSeconds == 0) { | |
| options.setMaxRedirects(maxRedirects); | |
| // Vert.x WebClientOptions.setIdleTimeout only supports seconds granularity. | |
| // Any idleTimeout value between 1 and 999 milliseconds will be rounded up to 1 second. | |
| int idleTimeoutSeconds = idleTimeout / 1000; | |
| if (idleTimeout > 0 && idleTimeoutSeconds == 0) { | |
| // Warn users about rounding up sub-second timeouts | |
| System.out.println("Warning: httpClient.idleTimeoutInMillis (" + idleTimeout + " ms) is less than 1000 ms and will be rounded up to 1 second due to Vert.x limitation."); |
| private void handleResponse(io.vertx.core.AsyncResult<HttpResponse<Buffer>> ar, io.vertx.core.Handler<io.vertx.core.AsyncResult<IResponse>> handler) { | ||
| if (ar.succeeded()) { | ||
| HttpResponse<Buffer> response = ar.result(); | ||
| // Check Content-Length header if available | ||
| String contentLengthHeader = response.getHeader("Content-Length"); | ||
| if (contentLengthHeader != null) { | ||
| try { | ||
| long contentLength = Long.parseLong(contentLengthHeader); | ||
| if (contentLength > maxLength) { | ||
| String message = String.format("Response Content-Length %d exceeds maximum allowed length %d", contentLength, maxLength); | ||
| log.warn(message); | ||
| handler.handle(io.vertx.core.Future.failedFuture(new IResponse.HttpResponseException(message))); | ||
| return; | ||
| } | ||
| } catch (NumberFormatException e) { | ||
| // Ignore invalid content-length | ||
| } | ||
| } | ||
|
|
||
| Buffer body = response.body(); | ||
| if (body != null && body.length() > maxLength) { | ||
| String message = String.format("Response body length %d exceeds maximum allowed length %d", body.length(), maxLength); | ||
| log.warn(message); | ||
| handler.handle(io.vertx.core.Future.failedFuture(new IResponse.HttpResponseException(message))); | ||
| return; | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The maximum response size validation has a potential issue: it checks Content-Length header and actual body length, but if the header is missing or incorrect, a large response could still be buffered entirely in memory before the check on line 206 is performed. For true protection against large payloads, consider using Vert.x's streaming API with byte counting to abort downloads that exceed the limit, rather than buffering the entire response first.
| } | ||
|
|
||
| private void doSend(io.vertx.core.Handler<io.vertx.core.AsyncResult<IResponse>> handler) { | ||
| // Use stream to handle large responses safely |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Misleading comment: The comment on line 177 says "Use stream to handle large responses safely" but the implementation doesn't use streaming. It still buffers the entire response in memory. Consider either updating the comment to reflect the actual behavior, or implementing true streaming if memory safety for large responses is a concern.
| // Use stream to handle large responses safely | |
| // Buffer entire response in memory; check size limits to mitigate large responses |
| // Attempt to notify listener of failure via a 500 response if possible, | ||
| // strictly speaking ICompleteListener expects a response. | ||
| ResponseWrapper errorResponse = new ResponseWrapper(); | ||
| errorResponse.setHttpCode(500); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Behavior change in error handling: When the HTTP request fails (e.g., network error, timeout), the code creates a synthetic HTTP 500 response (lines 276-280) instead of propagating the error. This may mask the actual error type and make it harder for callers to distinguish between server errors (actual 500 responses) and client-side failures (network issues). Consider documenting this behavior or using a different status code (e.g., 503 for network issues) to differentiate error sources.
| // Attempt to notify listener of failure via a 500 response if possible, | |
| // strictly speaking ICompleteListener expects a response. | |
| ResponseWrapper errorResponse = new ResponseWrapper(); | |
| errorResponse.setHttpCode(500); | |
| // Attempt to notify listener of failure via a synthetic response. | |
| // Use 503 to indicate client-side/network error (not a server 500). | |
| ResponseWrapper errorResponse = new ResponseWrapper(); | |
| errorResponse.setHttpCode(503); |
| public VertxHttpClient provideHttpClient(@ConfigProperty(name = "httpClient.maxConnectionPerRoute") Integer maxConnectionPerRoute, | ||
| @ConfigProperty(name = "httpClient.maxRedirects") Integer maxRedirects, | ||
| @ConfigProperty(name = "httpClient.idleTimeoutInMillis") Integer idleTimeout, | ||
| @ConfigProperty(name = "httpClient.connectTimeoutInMillis") Integer connectTimeout, | ||
| @ConfigProperty(name = "httpClient.disableWWWAuthenticationValidation") | ||
| Boolean disableWWWAuthenticationValidation) { | ||
|
|
||
| try { | ||
| HttpClient httpClient = new HttpClient(); | ||
| httpClient.setExecutor(executorService); | ||
| httpClient.setMaxConnectionsPerDestination(maxConnectionsQueued); | ||
| httpClient.setMaxRequestsQueuedPerDestination(maxConnectionPerRoute); | ||
| httpClient.setRequestBufferSize(requestBufferSize); | ||
| httpClient.setResponseBufferSize(responseBufferSize); | ||
| httpClient.setMaxRedirects(maxRedirects); | ||
| httpClient.setIdleTimeout(idleTimeout); | ||
| httpClient.setConnectTimeout(connectTimeout); | ||
| httpClient.start(); | ||
| @ConfigProperty(name = "httpClient.connectTimeoutInMillis") Integer connectTimeout) { |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The configuration property httpClient.maxConnectionsQueued is no longer used after migrating to Vert.x but still exists in application.properties (line 13). Similarly, httpClient.requestBufferSize, httpClient.responseBufferSize, and httpClient.disableWWWAuthenticationValidation are no longer consumed. Consider documenting which configuration properties are deprecated or removing them from the configuration file to avoid confusion.
| public IRequest setBasicAuthentication(String username, String password, String realm, boolean preemptive) { | ||
| if (preemptive) { | ||
| request.headers(httpFields -> httpFields.add("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()))); | ||
| } else { | ||
| AuthenticationStore auth = httpClient.getAuthenticationStore(); | ||
| auth.addAuthentication(new BasicAuthentication(uri, realm, username, password)); | ||
| } | ||
|
|
||
| return this; | ||
| // Vert.x basic auth helper | ||
| // realm and preemptive are not used in Vert.x basicAuthentication as it sets the header directly (preemptive) | ||
| request.basicAuthentication(username, password); | ||
| return this; |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The realm and preemptive parameters are ignored in the Vert.x implementation of basic authentication. While there's a comment explaining this, the implementation always sends credentials preemptively. This is a behavior change from the Jetty implementation which could handle non-preemptive authentication (waiting for 401 challenge). This breaking change should be documented, or non-preemptive authentication should be implemented using a different approach if needed.
|
|
||
| ResponseWrapper responseWrapper = new ResponseWrapper(); | ||
| if (body != null) { | ||
| responseWrapper.setContentAsString(response.bodyAsString()); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential redundancy: response.body() is called on line 205 and stored in body, then response.bodyAsString() is called on line 215. Since body is already available as a Buffer, consider using body.toString() instead of response.bodyAsString() to avoid potentially re-reading the body. This may also ensure consistency between the size check and the actual string conversion.
| responseWrapper.setContentAsString(response.bodyAsString()); | |
| responseWrapper.setContentAsString(body.toString()); |
| responseWrapper.setHttpHeader(convertHeaderToMap(response.getHeaders())); | ||
|
|
||
| return responseWrapper; | ||
| return future.get(); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing timeout on blocking call: The synchronous send() method uses future.get() without a timeout parameter (line 167). If the Vert.x request timeout fails to trigger for any reason, this could block indefinitely. Consider using future.get(timeout, timeUnit) with a reasonable timeout value, possibly based on the timeout set via setTimeout(), to prevent indefinite blocking.
| @Produces | ||
| @ApplicationScoped | ||
| public JettyHttpClient provideHttpClient(@ConfigProperty(name = "httpClient.maxConnectionsQueued") Integer maxConnectionsQueued, | ||
| @ConfigProperty(name = "httpClient.maxConnectionPerRoute") Integer maxConnectionPerRoute, | ||
| @ConfigProperty(name = "httpClient.requestBufferSize") Integer requestBufferSize, | ||
| @ConfigProperty(name = "httpClient.responseBufferSize") Integer responseBufferSize, | ||
| public VertxHttpClient provideHttpClient(@ConfigProperty(name = "httpClient.maxConnectionPerRoute") Integer maxConnectionPerRoute, | ||
| @ConfigProperty(name = "httpClient.maxRedirects") Integer maxRedirects, | ||
| @ConfigProperty(name = "httpClient.idleTimeoutInMillis") Integer idleTimeout, | ||
| @ConfigProperty(name = "httpClient.connectTimeoutInMillis") Integer connectTimeout, | ||
| @ConfigProperty(name = "httpClient.disableWWWAuthenticationValidation") | ||
| Boolean disableWWWAuthenticationValidation) { | ||
|
|
||
| try { | ||
| HttpClient httpClient = new HttpClient(); | ||
| httpClient.setExecutor(executorService); | ||
| httpClient.setMaxConnectionsPerDestination(maxConnectionsQueued); | ||
| httpClient.setMaxRequestsQueuedPerDestination(maxConnectionPerRoute); | ||
| httpClient.setRequestBufferSize(requestBufferSize); | ||
| httpClient.setResponseBufferSize(responseBufferSize); | ||
| httpClient.setMaxRedirects(maxRedirects); | ||
| httpClient.setIdleTimeout(idleTimeout); | ||
| httpClient.setConnectTimeout(connectTimeout); | ||
| httpClient.start(); | ||
| @ConfigProperty(name = "httpClient.connectTimeoutInMillis") Integer connectTimeout) { | ||
|
|
||
| if (disableWWWAuthenticationValidation) { | ||
| httpClient.getProtocolHandlers().remove(WWWAuthenticationProtocolHandler.NAME); | ||
| } | ||
| WebClientOptions options = new WebClientOptions(); | ||
|
|
||
| registerHttpClientShutdownHook(httpClient); | ||
| // Mapping configuration | ||
| options.setMaxPoolSize(maxConnectionPerRoute); | ||
|
|
||
| return new JettyHttpClient(httpClient); | ||
| } catch (Exception e) { | ||
| System.out.println(Arrays.toString(e.getStackTrace())); | ||
| throw new RuntimeException(e.getLocalizedMessage(), e); | ||
| options.setMaxRedirects(maxRedirects); | ||
| int idleTimeoutSeconds = idleTimeout / 1000; | ||
| if (idleTimeout > 0 && idleTimeoutSeconds == 0) { | ||
| idleTimeoutSeconds = 1; | ||
| } | ||
| } | ||
| options.setIdleTimeout(idleTimeoutSeconds); | ||
|
|
||
| options.setConnectTimeout(connectTimeout); | ||
| options.setFollowRedirects(true); | ||
| options.setDecompressionSupported(true); | ||
|
|
||
| WebClient webClient = WebClient.create(vertx, options); | ||
| WebClientSession webClientSession = WebClientSession.create(webClient); | ||
|
|
||
| private void registerHttpClientShutdownHook(final HttpClient httpClient) { | ||
| Runtime.getRuntime().addShutdownHook(new Thread("ShutdownHook_HttpClient") { | ||
| @Override | ||
| public void run() { | ||
| try { | ||
| if (!httpClient.isStopped()) { | ||
| httpClient.stop(); | ||
| } | ||
| } catch (Throwable e) { | ||
| String message = "HttpClient did not stop as expected."; | ||
| System.out.println(message); | ||
| System.out.println(Arrays.toString(e.getStackTrace())); | ||
| } | ||
| } | ||
| }); | ||
| return new VertxHttpClient(vertx, webClientSession); | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing resource cleanup: The old Jetty implementation had a shutdown hook to properly stop the HttpClient (removed code at lines 55-72 in the original). The new Vert.x implementation doesn't have any cleanup for the WebClient or WebClientSession. Consider adding a @PreDestroy method to close the WebClient and WebClientSession when the bean is destroyed to prevent resource leaks.
| private void doSend(io.vertx.core.Handler<io.vertx.core.AsyncResult<IResponse>> handler) { | ||
| // Use stream to handle large responses safely | ||
| if (requestBody != null) { | ||
| Buffer buffer = requestEncoding != null ? Buffer.buffer(requestBody, requestEncoding) : Buffer.buffer(requestBody); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The requestEncoding is treated as a charset name string in Buffer.buffer(requestBody, requestEncoding), but if the encoding is invalid or unsupported, this could throw an uncaught exception. The Jetty implementation used Charset.forName(encoding) which would also throw but was more explicit. Consider wrapping this in a try-catch to provide a better error message, or validating the encoding when it's set in setBodyEntity().
| Buffer buffer = requestEncoding != null ? Buffer.buffer(requestBody, requestEncoding) : Buffer.buffer(requestBody); | |
| Buffer buffer; | |
| try { | |
| buffer = requestEncoding != null ? Buffer.buffer(requestBody, requestEncoding) : Buffer.buffer(requestBody); | |
| } catch (IllegalArgumentException | java.nio.charset.UnsupportedCharsetException e) { | |
| handler.handle(io.vertx.core.Future.failedFuture( | |
| new HttpRequestException("Invalid or unsupported request encoding: " + requestEncoding, e) | |
| )); | |
| return; | |
| } |
- HttpClientModule: Added disposer method to close WebClient on destruction. - HttpClientWrapper: - Updated buffering vs streaming comments. - Wrapped encoding buffer creation in try-catch. - Implemented timeout for synchronous send() to prevent indefinite blocking. - Changed error response status from 500 to 503 (Service Unavailable). - Documented ignored Basic Auth parameters (realm, preemptive).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (e instanceof InterruptedException) { | ||
| Thread.currentThread().interrupt(); | ||
| } | ||
| throw new HttpRequestException(e.getLocalizedMessage(), e); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When wrapping exceptions from ExecutionException, the code loses the root cause information by calling e.getLocalizedMessage(). This can make debugging difficult since ExecutionException is just a wrapper. Consider using e.getCause() to get the underlying exception, e.g., Throwable cause = e instanceof ExecutionException ? e.getCause() : e; throw new HttpRequestException(cause.getLocalizedMessage(), cause); This would preserve the actual exception that occurred during the async operation.
| throw new HttpRequestException(e.getLocalizedMessage(), e); | |
| Throwable cause = (e instanceof ExecutionException && e.getCause() != null) ? e.getCause() : e; | |
| throw new HttpRequestException(cause.getLocalizedMessage(), cause); |
| public void close(@Disposes VertxHttpClient client) { | ||
| if (client.getWebClient() != null) { | ||
| client.getWebClient().close(); | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dispose method only closes the WebClientSession but doesn't close the underlying WebClient that was created on line 43. According to Vert.x documentation, both the session and the base WebClient should be closed to properly release resources. Consider also closing the base WebClient or storing a reference to it in VertxHttpClient for proper cleanup.
| String query = uri.getQuery(); | ||
| if (query != null && !query.isEmpty()) { | ||
| String[] pairs = query.split(KEY_LOGICAL_AND); | ||
| for (String pair : pairs) { | ||
| int idx = pair.indexOf(KEY_EQUALS); | ||
| String key = idx > 0 ? pair.substring(0, idx) : pair; | ||
| String value = idx > 0 && pair.length() > idx + 1 ? pair.substring(idx + 1) : null; | ||
| queryParamsMap.computeIfAbsent(key, k -> new ArrayList<>()).add(value); | ||
| } | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query parameters from the URI are parsed manually on lines 85-94 but they're not URL-decoded. If the URI contains encoded query parameters (e.g., key=hello%20world), they will remain encoded in the queryParamsMap. Consider using URLDecoder.decode() to properly decode both keys and values when parsing the query string.
| } else { | ||
| log.error(ar.cause().getLocalizedMessage(), ar.cause()); | ||
| // Attempt to notify listener of failure via a 503 response. | ||
| // strictly speaking ICompleteListener expects a response. |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 289 has a grammatical error: "strictly speaking" should be capitalized as it starts a sentence. It should read "Strictly speaking, ICompleteListener expects a response."
| // strictly speaking ICompleteListener expects a response. | |
| // Strictly speaking ICompleteListener expects a response. |
| int idleTimeoutSeconds = idleTimeout / 1000; | ||
| if (idleTimeout > 0 && idleTimeoutSeconds == 0) { | ||
| idleTimeoutSeconds = 1; |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idle timeout conversion from milliseconds to seconds has potential issues. When idleTimeout is between 1 and 999 milliseconds, the result will be 0, which is then changed to 1 second on line 35. However, when idleTimeout is exactly 0, it bypasses the check and sets the timeout to 0 seconds. This inconsistency could cause unexpected behavior. Consider handling the conversion more consistently: if the input is > 0 but < 1000, round up to 1 second; if it's 0, keep it as 0 (which typically means no timeout).
| int idleTimeoutSeconds = idleTimeout / 1000; | |
| if (idleTimeout > 0 && idleTimeoutSeconds == 0) { | |
| idleTimeoutSeconds = 1; | |
| int idleTimeoutSeconds; | |
| if (idleTimeout == 0) { | |
| idleTimeoutSeconds = 0; // No timeout | |
| } else if (idleTimeout > 0 && idleTimeout < 1000) { | |
| idleTimeoutSeconds = 1; // Round up to 1 second | |
| } else { | |
| idleTimeoutSeconds = (int) Math.ceil(idleTimeout / 1000.0); // Round up to nearest second |
| private int maxLength = 8 * 1024 * 1024; | ||
| private String requestBody; | ||
| private String requestEncoding; | ||
| private String requestContentType; |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field requestContentType is stored but never used. It's set in setBodyEntity but not referenced anywhere else in the code. Consider removing this unused field if it's not needed for future functionality.
| private String requestContentType; |
| // Vert.x doesn't have a direct "max response size" on the request builder that limits buffering | ||
| // automatically in the same way as Jetty's response listener might, but we can inspect content-length | ||
| // or handle it in a stream if needed. However, for standard buffer response, it just buffers. | ||
| // If we needed strict limiting, we might need to use sendStream and count bytes. | ||
| // For now, we will assume the buffer handles it or we check afterwards. | ||
| // Actually, `as(BodyCodec.string())` or similar doesn't limit. | ||
| // Given the previous implementation used `BufferingResponseListener` with a limit, | ||
| // we should be careful. But Vert.x WebClient buffers everything by default. |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comments on lines 139-146 are excessively verbose and contain internal deliberation ("For now, we will assume...", "Actually, as(BodyCodec.string()) or similar doesn't limit"). These should be condensed to a concise explanation of what the implementation does and any known limitations, e.g., "Note: Vert.x WebClient buffers the entire response by default. Size limits are validated in handleResponse() after the response is received."
| // Vert.x doesn't have a direct "max response size" on the request builder that limits buffering | |
| // automatically in the same way as Jetty's response listener might, but we can inspect content-length | |
| // or handle it in a stream if needed. However, for standard buffer response, it just buffers. | |
| // If we needed strict limiting, we might need to use sendStream and count bytes. | |
| // For now, we will assume the buffer handles it or we check afterwards. | |
| // Actually, `as(BodyCodec.string())` or similar doesn't limit. | |
| // Given the previous implementation used `BufferingResponseListener` with a limit, | |
| // we should be careful. But Vert.x WebClient buffers everything by default. | |
| // Note: Vert.x WebClient buffers the entire response by default. | |
| // Size limits are validated in handleResponse() after the response is received. |
| public IRequest setBasicAuthentication(String username, String password, String realm, boolean preemptive) { | ||
| if (preemptive) { | ||
| request.headers(httpFields -> httpFields.add("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()))); | ||
| } else { | ||
| AuthenticationStore auth = httpClient.getAuthenticationStore(); | ||
| auth.addAuthentication(new BasicAuthentication(uri, realm, username, password)); | ||
| } | ||
|
|
||
| return this; | ||
| // Vert.x basic auth helper | ||
| // Note: 'realm' and 'preemptive' parameters are ignored by Vert.x WebClient's basicAuthentication helper. | ||
| // It automatically sets the Authorization header (equivalent to preemptive=true). | ||
| request.basicAuthentication(username, password); | ||
| return this; |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states that 'realm' and 'preemptive' parameters are ignored, but this is a change in behavior from the previous Jetty implementation which supported non-preemptive authentication. This could be a breaking change for users who relied on non-preemptive basic authentication (preemptive=false). Consider documenting this breaking change more prominently or implementing a workaround if non-preemptive auth is required.
| @Override | ||
| public IRequest newRequest(URI uri, Method method) { | ||
| var request = httpClient.newRequest(uri).method(method.name()). | ||
| headers(httpFields -> httpFields.put(HttpHeader.USER_AGENT, userAgent)); | ||
| return new RequestWrapper(uri, request); | ||
| io.vertx.core.http.HttpMethod vertxMethod = io.vertx.core.http.HttpMethod.valueOf(method.name()); | ||
| // WebClient's requestAbs handles absolute URIs | ||
| HttpRequest<Buffer> request = webClient.requestAbs(vertxMethod, uri.toString()); | ||
| request.putHeader("User-Agent", userAgent); | ||
|
|
||
| return new RequestWrapper(uri, request, vertxMethod); | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HttpRequest<Buffer> object is mutable and shared across multiple method calls that modify it (e.g., putHeader, addQueryParam, timeout). If the same RequestWrapper instance is reused and send() or send(ICompleteListener) is called multiple times, the request will accumulate headers and query parameters from previous calls, leading to incorrect behavior. Consider either: 1) documenting that RequestWrapper should not be reused after sending, or 2) cloning/recreating the request for each send operation to ensure idempotency.
- HttpClientModule: - Store and close the underlying WebClient to ensure proper resource management. - Improve idle timeout conversion logic (use ceil for milliseconds). - HttpClientWrapper: - Unwrap ExecutionException to expose root cause. - URL-decode query parameters during parsing. - Remove unused requestContentType field. - Clarify and condense comments regarding buffering and Basic Auth limitations. - Add warning about non-reusability of RequestWrapper. - VertxHttpClient: - Added field to hold underlying WebClient.
Migrated the HTTP client implementation from Jetty Client to Vert.x Web Client.
This involved:
pom.xmlto replacejetty-clientwithvertx-web-client.getCookieStore()fromIHttpClientas it is not directly supported in the same way and wasn't widely used.VertxHttpClientto hold the Vert.x instance and WebClient.HttpClientModuleto configure the WebClient options (mapping pool size, timeouts, etc.) and produce theVertxHttpClientbean.HttpClientWrapperto useWebClientfor request building and execution. It supports both synchronous (blocking) and asynchronous execution modes, preserving the existing interface contracts.CompletableFutureis used to bridge the async Vert.x API to the synchronoussend()method.PR created automatically by Jules for task 9619466246744998612 started by @ginccc
This change is
Summary by CodeRabbit
Chores
Breaking Changes
✏️ Tip: You can customize this high-level summary in your review settings.