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


##########
components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java:
##########
@@ -85,6 +90,7 @@ public class HttpComponent extends HttpCommonComponent 
implements RestProducerFa
 
     private static final Logger LOG = 
LoggerFactory.getLogger(HttpComponent.class);
     private static final String TARGET_URI_PARAMETER = 
HttpComponent.class.getName() + ".targetUri";
+    private static final boolean BROTLI4J_AVAILABLE = isBrotli4jAvailable();

Review Comment:
   🔴 **Critical — over-broad activation:** `isBrotli4jAvailable()` returns 
`false` when brotli4j is not on the classpath at all (`ClassNotFoundException` 
→ `false`). So `!BROTLI4J_AVAILABLE` is `true` for the vast majority of Camel 
HTTP users who simply don't use brotli4j, and they all get the custom decoder 
map (which is missing `x-gzip`).
   
   The condition needs to distinguish between:
   - **brotli4j absent** → do nothing, HttpClient's defaults are correct
   - **brotli4j API present, native library missing** → install the workaround
   
   As Croway suggested, the condition should be something like:
   ```java
   private static final boolean BROTLI4J_UNUSABLE = isBrotliDecoderUnusable();
   
   static boolean isBrotliDecoderUnusable() {
       try {
           // Check if brotli4j API is on the classpath
           Class.forName("com.aayushatharva.brotli4j.Brotli4jLoader");
       } catch (ClassNotFoundException e) {
           return false; // Not on classpath at all — HttpClient defaults are 
fine
       }
       // API is present, check if native library is loadable
       return !isBrotli4jAvailable();
   }
   ```



##########
components/camel-http/src/main/java/org/apache/camel/component/http/HttpComponent.java:
##########
@@ -664,6 +677,35 @@ protected HttpClientBuilder createHttpClientBuilder(
         return clientBuilder;
     }
 
+    static boolean isBrotli4jAvailable() {
+        try {
+            Class<?> loader = 
Class.forName("com.aayushatharva.brotli4j.Brotli4jLoader", true,
+                    HttpComponent.class.getClassLoader());
+            return (boolean) loader.getMethod("isAvailable").invoke(null);
+        } catch (Exception | LinkageError e) {
+            return false;
+        }
+    }
+
+    /**
+     * Builds a content decoder registry containing all HttpClient-supported 
decoders except Brotli. Called when the
+     * Brotli4j API jar is on the classpath but the platform-native JNI 
library is missing.
+     */
+    static LinkedHashMap<String, InputStreamFactory> 
buildDecodersWithoutBrotli() {
+        LinkedHashMap<String, InputStreamFactory> decoders = new 
LinkedHashMap<>();
+        for (ContentCoding cc : ContentCoding.values()) {
+            if (cc == ContentCoding.BROTLI) {
+                continue;
+            }
+            if (ContentCodecRegistry.decoder(cc) != null) {
+                final ContentCoding coding = cc;
+                decoders.put(cc.token(), in -> 
ContentCodecRegistry.unwrap(coding,
+                        new BasicHttpEntity(in, null)).getContent());
+            }
+        }
+        return decoders;
+    }

Review Comment:
   🔴 **Critical — missing `x-gzip` alias:** `ContentCodecRegistry` does not 
register a decoder for `ContentCoding.X_GZIP`. HttpClient's default 
[`ContentCompressionExec`](https://github.com/apache/httpcomponents-client/blob/5.6.x/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java#L106-L110)
 adds `x-gzip` as a separate alias of the `GZIP` decoder, but this method 
doesn't replicate that.
   
   Result: a server responding with `Content-Encoding: x-gzip` triggers 
`HttpException: Unsupported Content-Encoding: x-gzip`.
   
   Fix: after the loop, add the `x-gzip` alias:
   
   ```suggestion
       static LinkedHashMap<String, InputStreamFactory> 
buildDecodersWithoutBrotli() {
           LinkedHashMap<String, InputStreamFactory> decoders = new 
LinkedHashMap<>();
           for (ContentCoding cc : ContentCoding.values()) {
               if (cc == ContentCoding.BROTLI) {
                   continue;
               }
               if (ContentCodecRegistry.decoder(cc) != null) {
                   final ContentCoding coding = cc;
                   decoders.put(cc.token(), in -> 
ContentCodecRegistry.unwrap(coding,
                           new BasicHttpEntity(in, null)).getContent());
               }
           }
           // Replicate the x-gzip alias that ContentCompressionExec registers 
by default
           if (decoders.containsKey(ContentCoding.GZIP.token())) {
               decoders.put(ContentCoding.X_GZIP.token(), 
decoders.get(ContentCoding.GZIP.token()));
           }
           return decoders;
       }
   ```



##########
components/camel-http/src/test/java/org/apache/camel/component/http/HttpBrotli4jAvailabilityTest.java:
##########
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.http;
+
+import java.util.LinkedHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.aayushatharva.brotli4j.Brotli4jLoader;
+import org.apache.hc.client5.http.entity.InputStreamFactory;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
+import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class HttpBrotli4jAvailabilityTest extends BaseHttpTest {
+
+    private HttpServer localServer;
+    private final AtomicReference<String> capturedAcceptEncoding = new 
AtomicReference<>();
+
+    @Override
+    public void setupResources() throws Exception {
+        HttpRequestHandler handler = (request, response, context) -> {
+            capturedAcceptEncoding.set(
+                    request.getFirstHeader("Accept-Encoding") != null
+                            ? 
request.getFirstHeader("Accept-Encoding").getValue()
+                            : null);
+            response.setCode(HttpStatus.SC_OK);
+            response.setEntity(new StringEntity(getExpectedContent()));
+        };
+
+        localServer = ServerBootstrap.bootstrap()
+                .setCanonicalHostName("localhost")
+                .register("/", handler)
+                .create();
+        localServer.start();
+    }
+
+    @Override
+    public void cleanupResources() throws Exception {
+        if (localServer != null) {
+            localServer.stop();
+        }
+    }
+
+    @Test
+    void brotli4jAvailabilityShouldMatchLoaderState() {
+        // The brotli4j API jar is on the test classpath (test-scope 
dependency).
+        // Whether the native library loads depends on the test machine (e.g. 
Homebrew
+        // brotli on macOS). What matters is that our reflective check returns 
the same
+        // result as calling Brotli4jLoader.isAvailable() directly.
+        assertThat(HttpComponent.isBrotli4jAvailable())
+                .as("isBrotli4jAvailable() must agree with 
Brotli4jLoader.isAvailable()")
+                .isEqualTo(Brotli4jLoader.isAvailable());
+    }
+
+    @Test
+    void brotli4jLoaderClassShouldBeOnClasspath() {
+        // Verify the API jar is actually on the test classpath, so the test 
above
+        // is validating native-lib detection, not just a missing class.
+        try {
+            Class.forName("com.aayushatharva.brotli4j.Brotli4jLoader");
+        } catch (ClassNotFoundException e) {
+            throw new AssertionError("brotli4j API jar should be on the test 
classpath as a test-scope dependency", e);
+        }
+    }
+
+    @Test
+    void buildDecodersWithoutBrotliShouldExcludeBr() {
+        LinkedHashMap<String, InputStreamFactory> decoders = 
HttpComponent.buildDecodersWithoutBrotli();
+        assertThat(decoders).doesNotContainKey("br");
+        assertThat(decoders).containsKey("gzip");
+        assertThat(decoders).containsKey("deflate");
+    }

Review Comment:
   ⚠️ **Test gap:** This test doesn't verify `x-gzip` decompression. A test 
that sends a `Content-Encoding: x-gzip` response and verifies successful 
decompression would have caught the regression. Also, 
`buildDecodersWithoutBrotliShouldExcludeBr` should assert `x-gzip` is present.
   
   More broadly, on CI the native brotli4j jar is typically available (Maven 
activates the OS-specific profiles for the test-scoped dependency), so 
`Brotli4jLoader.isAvailable()` returns `true` and the "unavailable" branch of 
`acceptEncodingShouldReflectBrotli4jAvailability` is never exercised. Consider 
the approach Croway described: exclude the native transitive dependencies and 
only include the `service` jar, forcing the "API present, native missing" state 
on all CI platforms.



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