gnodet-bot commented on code in PR #29804:
URL: https://github.com/apache/camel/pull/29804#discussion_r4565312988


##########
dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/GistHelper.java:
##########
@@ -83,11 +88,42 @@ private static void doFetchGistUrls(
     static void resolveGistAsRawFiles(
             String url, StringJoiner routes, StringJoiner kamelets, 
StringJoiner properties, StringJoiner all)
             throws Exception {
+        resolveGistAsRawFiles(url, routes, kamelets, properties, all, 
Duration.ofSeconds(20));
+    }
+
+    /**
+     * Package-private overload accepting a custom timeout for testing.
+     */
+    static void resolveGistAsRawFiles(
+            String url, StringJoiner routes, StringJoiner kamelets, 
StringJoiner properties, StringJoiner all,
+            Duration timeout)
+            throws Exception {
 
         // use JDK http client to call github api
         HttpClient hc = 
HttpClient.newBuilder().proxy(ProxySelector.getDefault()).build();
-        HttpResponse<String> res = hc.send(HttpRequest.newBuilder(new 
URI(url)).timeout(Duration.ofSeconds(20)).build(),
-                HttpResponse.BodyHandlers.ofString());
+        HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(new 
URI(url)).timeout(timeout);
+        // Send GITHUB_TOKEN as Authorization header when set, but only to 
api.github.com (never leak to other hosts)
+        String token = System.getenv("GITHUB_TOKEN");
+        if (token != null && !token.isBlank() && "api.github.com".equals(new 
URI(url).getHost())) {

Review Comment:
   💡 **`new URI(url)` parsed twice** — `new URI(url)` appears here and on the 
`reqBuilder` line above. Extract to a local variable `URI uri = new URI(url);` 
before the `reqBuilder` line and use `uri.getHost()` here. The second parse is 
redundant and can itself throw `URISyntaxException`.
   
   ```suggestion
           if (token != null && !token.isBlank() && 
"api.github.com".equals(uri.getHost())) {
   ```
   (also change line 104 to use `uri` instead of `new URI(url)` — requires a 
two-line suggestion block)



##########
dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/GitHubHelper.java:
##########
@@ -152,11 +157,42 @@ private static void addSingleFileUrl(
     static void resolveGithubAsRawFiles(
             String url, String wildcard, StringJoiner routes, StringJoiner 
kamelets, StringJoiner properties, StringJoiner all)
             throws Exception {
+        resolveGithubAsRawFiles(url, wildcard, routes, kamelets, properties, 
all, Duration.ofSeconds(20));
+    }
+
+    /**
+     * Package-private overload accepting a custom timeout for testing.
+     */
+    static void resolveGithubAsRawFiles(
+            String url, String wildcard, StringJoiner routes, StringJoiner 
kamelets, StringJoiner properties, StringJoiner all,
+            Duration timeout)
+            throws Exception {
 
         // use JDK http client to call github api
         HttpClient hc = 
HttpClient.newBuilder().proxy(ProxySelector.getDefault()).build();
-        HttpResponse<String> res = hc.send(HttpRequest.newBuilder(new 
URI(url)).timeout(Duration.ofSeconds(20)).build(),
-                HttpResponse.BodyHandlers.ofString());
+        HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(new 
URI(url)).timeout(timeout);
+        // Send GITHUB_TOKEN as Authorization header when set, but only to 
api.github.com (never leak to other hosts)
+        String token = System.getenv("GITHUB_TOKEN");
+        if (token != null && !token.isBlank() && "api.github.com".equals(new 
URI(url).getHost())) {

Review Comment:
   💡 **`new URI(url)` parsed twice** — same pattern as `GistHelper.java`: `new 
URI(url)` is called here and on the `reqBuilder` line above. Extract to `URI 
uri = new URI(url);` before `reqBuilder` and use `uri.getHost()` here.
   
   ```suggestion
           if (token != null && !token.isBlank() && 
"api.github.com".equals(uri.getHost())) {
   ```



##########
dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/selfupdate/SelfUpdateCommand.java:
##########
@@ -368,10 +373,32 @@ private Integer runInstaller(String targetVersion) {
             // pin-state file for this run. Set last so it can never be 
dropped by the override block above.
             pb.environment().put("CAMEL_INSTALL_SELF_UPDATE", "true");
             Process process = pb.start();
-            String output = new 
String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
-            int exit = process.waitFor();
-            printer().print(output);
-            return exit;
+            // Drain stdout in a separate thread to prevent the subprocess 
from blocking on a full pipe buffer in
+            // the gap before waitFor() returns. Then wait with a timeout to 
prevent an indefinitely stalled
+            // installer from hanging the CLI.
+            ExecutorService drainer = Executors.newSingleThreadExecutor(r -> {
+                Thread t = new Thread(r, "camel-self-update-drainer");
+                t.setDaemon(true);
+                return t;
+            });
+            Future<String> outputFuture = drainer
+                    .submit(() -> new 
String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
+            try {
+                boolean finished = process.waitFor(5, TimeUnit.MINUTES);
+                if (!finished) {
+                    // Kill grandchildren first (curl/wget etc.) before the 
shell, so they release the pipe
+                    
process.descendants().forEach(ProcessHandle::destroyForcibly);
+                    process.destroyForcibly();
+                    printer().printErr("installer timed out after 5 minutes");
+                    return CliExitCode.TIMEOUT;
+                }
+                String output = outputFuture.get(10, TimeUnit.SECONDS);
+                int exit = process.exitValue();
+                printer().print(output);
+                return exit;
+            } finally {
+                drainer.shutdown();

Review Comment:
   âš ī¸ **`shutdown()` won't interrupt a blocked `readAllBytes()`** — on the 
timeout path the process is killed, but the drainer thread may be stuck in 
`readAllBytes()` waiting for more data. `shutdown()` waits for running tasks to 
complete naturally; it does not interrupt them. Use `shutdownNow()` to 
interrupt the thread.
   
   ```suggestion
                   drainer.shutdownNow();
   ```



##########
dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/common/GitHubApiResponseTest.java:
##########
@@ -94,4 +104,95 @@ void trailingContentAndNonTextNamesAreTolerated() throws 
Exception {
 
         
assertThat(all).hasToString(GitHubHelper.asGithubSingleUrl("https://github.com/me/repo/blob/main/route.java";));
     }
+
+    @Test
+    void nonOkGithubResponseThrows() {
+        statusCode = 404;
+
+        assertThatThrownBy(() -> GitHubHelper.resolveGithubAsRawFiles(url, 
null, null, null, null, new StringJoiner(",")))
+                .isInstanceOf(IOException.class)
+                .hasMessageContaining("404");
+    }
+
+    @Test
+    void rateLimitedGithubResponseThrowsWithHint() {

Review Comment:
   đŸˇī¸ **Misleading test name** — this test sends HTTP 403 without an 
`x-ratelimit-remaining: 0` header, so `rateLimitHint()` returns the 
access-denied message, not a rate-limit message. The assertion confirms 
`"access denied"`. Consider renaming to 
`accessDeniedGithubResponseThrowsWithHint` and adding a separate test for the 
actual rate-limit case (403 + `x-ratelimit-remaining: 0`).



##########
dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/GistHelper.java:
##########
@@ -115,7 +151,35 @@ static void resolveGistAsRawFiles(
                     all.add(u);
                 }
             }
+        } else if (res.statusCode() == 404) {
+            throw new FileNotFoundException("GitHub Gist API request failed 
with HTTP 404 for URL: " + url);
+        } else {
+            String hint = rateLimitHint(res, token);
+            throw new IOException(
+                    "GitHub Gist API request failed with HTTP " + 
res.statusCode() + hint + " for URL: " + url);
+        }
+    }
+
+    /**
+     * Returns a hint string for rate-limit or access-denied errors (403/429).
+     */
+    private static String rateLimitHint(HttpResponse<String> res, String 
token) {

Review Comment:
   🔁 **`rateLimitHint()` duplicated in `GistHelper` and `GitHubHelper`** — both 
classes are in the same package and contain identical implementations. Consider 
extracting to a package-private utility (e.g. `GitHubHttpHelper`) to avoid 
divergence.



##########
dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/selfupdate/SelfUpdateCommand.java:
##########
@@ -368,10 +373,32 @@ private Integer runInstaller(String targetVersion) {
             // pin-state file for this run. Set last so it can never be 
dropped by the override block above.
             pb.environment().put("CAMEL_INSTALL_SELF_UPDATE", "true");
             Process process = pb.start();
-            String output = new 
String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
-            int exit = process.waitFor();
-            printer().print(output);
-            return exit;
+            // Drain stdout in a separate thread to prevent the subprocess 
from blocking on a full pipe buffer in
+            // the gap before waitFor() returns. Then wait with a timeout to 
prevent an indefinitely stalled
+            // installer from hanging the CLI.
+            ExecutorService drainer = Executors.newSingleThreadExecutor(r -> {
+                Thread t = new Thread(r, "camel-self-update-drainer");
+                t.setDaemon(true);
+                return t;
+            });
+            Future<String> outputFuture = drainer
+                    .submit(() -> new 
String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
+            try {
+                boolean finished = process.waitFor(5, TimeUnit.MINUTES);
+                if (!finished) {
+                    // Kill grandchildren first (curl/wget etc.) before the 
shell, so they release the pipe
+                    
process.descendants().forEach(ProcessHandle::destroyForcibly);
+                    process.destroyForcibly();
+                    printer().printErr("installer timed out after 5 minutes");
+                    return CliExitCode.TIMEOUT;
+                }
+                String output = outputFuture.get(10, TimeUnit.SECONDS);

Review Comment:
   â„šī¸ **Misleading error message on stdout-drain timeout** — 
`outputFuture.get(10, TimeUnit.SECONDS)` can throw `TimeoutException` or 
`ExecutionException`, which are caught by the outer `catch (Exception e)` 
block. That outer handler prints `"failed to run the installer"`, but the 
install may have actually succeeded (process exited 0). Consider catching these 
explicitly here and printing a more accurate message like `"installer completed 
but stdout drain timed out"` to avoid confusing users.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to