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


##########
components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIWebhookConsumer.java:
##########
@@ -0,0 +1,321 @@
+/*
+ * 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.openai;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import com.openai.core.ClientOptions;
+import com.openai.core.RequestOptions;
+import com.openai.core.http.Headers;
+import com.openai.core.http.HttpClient;
+import com.openai.core.http.HttpRequest;
+import com.openai.core.http.HttpResponse;
+import com.openai.errors.InvalidWebhookSignatureException;
+import com.openai.errors.OpenAIInvalidDataException;
+import com.openai.models.webhooks.UnwrapWebhookEvent;
+import com.openai.models.webhooks.WebhookVerificationParams;
+import com.openai.services.blocking.WebhookService;
+import com.openai.services.blocking.WebhookServiceImpl;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.Processor;
+import org.apache.camel.spi.RestConfiguration;
+import org.apache.camel.spi.RestConsumerFactory;
+import org.apache.camel.support.DefaultConsumer;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Receives the webhook events OpenAI sends: it serves one HTTP endpoint 
through the REST consumer factory of the
+ * runtime (platform-http unless another component is configured), verifies 
the signature of each request against the
+ * webhook secret, and runs the route with the parsed event.
+ */
+public class OpenAIWebhookConsumer extends DefaultConsumer {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenAIWebhookConsumer.class);
+
+    private static final String[] SIGNATURE_HEADERS = { "webhook-id", 
"webhook-timestamp", "webhook-signature" };
+
+    /**
+     * The SDK requires a credential to build its options, while verifying a 
signature needs the webhook secret alone
+     * and sends nothing. This one is never used, and a route that just 
receives events needs no API key.
+     */
+    private static final String UNUSED_API_KEY = 
"openai-webhook-verification-only";
+
+    private final OpenAIEndpoint endpoint;
+
+    private WebhookService webhooks;
+    private Consumer httpConsumer;
+
+    public OpenAIWebhookConsumer(OpenAIEndpoint endpoint, Processor processor) 
{
+        super(endpoint, processor);
+        this.endpoint = endpoint;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+
+        OpenAIConfiguration configuration = endpoint.getConfiguration();
+        String secret = configuration.getWebhookSecret();
+        if (ObjectHelper.isEmpty(secret)) {
+            throw new IllegalArgumentException(
+                    "webhookSecret is required by the webhook operation: it is 
the signing secret of the endpoint in"
+                                               + " the OpenAI dashboard, and 
without it an event cannot be told from a forged request");
+        }
+
+        RestConsumerFactory factory = resolveRestConsumerFactory();
+        if (factory == null) {
+            throw new IllegalStateException(
+                    "No RestConsumerFactory found. The webhook operation needs 
camel-platform-http, or another HTTP"
+                                            + " server component named in 
httpServerComponent or in the rest configuration");
+        }
+
+        webhooks = createWebhookService(secret);
+
+        String path = configuration.getWebhookPath();
+        RestConfiguration restConfiguration = 
endpoint.getCamelContext().getRestConfiguration();
+        httpConsumer = factory.createConsumer(endpoint.getCamelContext(), 
this::onWebhookRequest,
+                "POST", path, null, "application/json", null, 
restConfiguration, Collections.emptyMap());
+        endpoint.configureNestedConsumer(httpConsumer);
+        ServiceHelper.startService(httpConsumer);
+
+        LOG.debug("OpenAI webhook consumer listening on POST {}", path);
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        ServiceHelper.stopService(httpConsumer);
+        httpConsumer = null;
+        webhooks = null;
+        super.doStop();
+    }
+
+    /**
+     * Answers the HTTP request of OpenAI. The route never sees an event whose 
signature did not verify, and it runs on
+     * an exchange of its own, so that no header of the request reaches it.
+     */
+    private void onWebhookRequest(Exchange httpExchange) {
+        byte[] payload;
+        try {
+            payload = readPayload(httpExchange);
+        } catch (IOException e) {
+            LOG.debug("Rejected an OpenAI webhook request: {}", 
e.getMessage());
+            respond(httpExchange, 413, "Webhook request too large");
+            return;
+        }

Review Comment:
   ⚠️ **`IOException` from transport errors is answered 413 instead of 500.**
   
   `readPayload` throws `IOException` from two distinct sources:
   1. `in.readNBytes(max + 1)` — any transport/network error (connection reset, 
client abort)
   2. The explicit size-limit check: `new IOException("The request body is 
larger than...")`
   
   Both land here and are answered 413 "Webhook request too large". A 
connection reset mid-read is not a 413 — it should answer nothing (or 500, 
which OpenAI retries). Telling OpenAI 413 on a transient error teaches it to 
give up permanently.
   
   Fix: split the limit case from actual I/O errors by using a dedicated 
exception:
   
   ```suggestion
           byte[] payload;
           try {
               payload = readPayload(httpExchange);
           } catch (PayloadTooLargeException e) {
               LOG.debug("Rejected an OpenAI webhook request: {}", 
e.getMessage());
               respond(httpExchange, 413, "Webhook request too large");
               return;
           }
   ```
   
   And in `readPayload`, replace the `IOException` thrown for the size limit:
   
   ```java
   private static final class PayloadTooLargeException extends RuntimeException 
{
       PayloadTooLargeException(int max) {
           super("The request body is larger than webhookMaxPayloadSize (" + 
max + " bytes)");
       }
   }
   ```
   
   Transport `IOException`s from `readNBytes` propagate uncaught here and fall 
through to the outer `catch (Exception e)` block, which answers 500 and lets 
OpenAI retry.



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