This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new f8fb7bf55452 CAMEL-26829: Preserve per-item OPA batch decision 
outcomes (#29637)
f8fb7bf55452 is described below

commit f8fb7bf55452fff68f8016141250adac15936710
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 12 21:45:50 2026 +0100

    CAMEL-26829: Preserve per-item OPA batch decision outcomes (#29637)
    
    Preserve the Boolean batch-result list and add immutable per-item outcomes 
containing the normalized decision, sanitized failure category and fail-open 
flag. Set the aggregate failed-open header when any element used fail-open 
handling.
    
    Outcomes have a readable diagnostic representation and support 
serialization in persisted exchange headers. Documents the shared outcome 
semantics for whole-batch and per-element unavailability, with 
component/catalog and migration documentation.
    
    Codex on behalf of oscerd.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Co-authored-by: Codex <[email protected]>
---
 .../org/apache/camel/catalog/components/opa.json   |   5 +-
 .../apache/camel/catalog/docs/opa-component.adoc   |  22 +++++
 .../org/apache/camel/component/opa/opa.json        |   5 +-
 .../camel-opa/src/main/docs/opa-component.adoc     |  22 +++++
 .../camel/component/opa/OpaBatchOutcome.java       |  86 +++++++++++++++++
 .../apache/camel/component/opa/OpaConstants.java   |  10 +-
 .../camel/component/opa/OpaPolicyEvaluator.java    |  67 +++++++++-----
 .../camel/component/opa/OpaRestEvaluator.java      |   6 +-
 .../component/opa/OpaBatchEvaluationTest.java      | 102 +++++++++++++++++++++
 .../camel/component/opa/OpaBatchOutcomeTest.java   |  64 +++++++++++++
 .../camel/component/opa/OpaInputDocumentTest.java  |   3 +-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   7 ++
 .../endpoint/dsl/OpaEndpointBuilderFactory.java    |  20 +++-
 13 files changed, 387 insertions(+), 32 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
index 6d841fa2a489..7eec7e2fdf00 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/opa.json
@@ -52,8 +52,9 @@
     "CamelOpaDecisionAllow": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "autowired": false, "secret": false, 
"description": "The allow\/deny verdict of the policy evaluation. Always 
overwritten by the component, so a value set by an inbound message never 
survives into the route.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION_ALLOW" },
     "CamelOpaDecision": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Object", "deprecated": false, "autowired": false, "secret": false, 
"description": "The raw decision document returned by OPA. Useful for policies 
that return more than a boolean, such as obligations, row filters or deny 
reasons.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION" },
     "CamelOpaPolicyPath": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "autowired": false, "secret": false, 
"description": "The policy path that was evaluated. Set by the component for 
observability; it is not read as an input and cannot be used to select a 
different policy.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#POLICY_PATH" },
-    "CamelOpaDecisionFailedOpen": { "index": 3, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "autowired": false, "secret": 
false, "description": "Set to true only when the exchange proceeded because 
failOpen is enabled and the policy could not be evaluated - nothing authorized 
it. Absent on every decision an actual policy made, so a route or an audit 
trail can tell the two apart rather than s [...]
-    "CamelOpaBatchDecision": { "index": 4, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"java.util.List<Boolean>", "deprecated": false, "autowired": false, "secret": 
false, "description": "The per-element allow\/deny verdicts of a batch 
evaluation (batch=true), as a List of Boolean parallel to the List body. Always 
overwritten by the component. An element whose evaluation could not be reached 
is denied, unless failOpen is s [...]
+    "CamelOpaDecisionFailedOpen": { "index": 3, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "autowired": false, "secret": 
false, "description": "Set to true only when the exchange proceeded because 
failOpen is enabled and the policy could not be evaluated - nothing authorized 
it. Absent on every decision an actual policy made, so a route or an audit 
trail can tell the two apart rather than s [...]
+    "CamelOpaBatchDecision": { "index": 4, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"java.util.List<Boolean>", "deprecated": false, "autowired": false, "secret": 
false, "description": "The per-element allow\/deny verdicts of a batch 
evaluation (batch=true), as a List of Boolean parallel to the List body. Always 
overwritten by the component. An element whose evaluation could not be reached 
is denied, unless failOpen is s [...]
+    "CamelOpaBatchOutcomes": { "index": 5, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"java.util.List<org.apache.camel.component.opa.OpaBatchOutcome>", "deprecated": 
false, "autowired": false, "secret": false, "description": "Immutable 
per-element outcomes of a batch evaluation, parallel to the List body. Each 
outcome exposes the effective allowed verdict, nullable Boolean policy 
decision, failure category and failedOpen  [...]
   },
   "bodyTypes": {
     "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "batch", 
"value": "true", "carriers": [ "list" ] } ], "description": "Not read unless 
includeBody=true: then a String, number, boolean, Map or List goes into the 
input document as it is, and another body as its string form (a POJO as its 
toString). With batch=true a List, one input document per element: another body 
fails" },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
index 6c5f3077c2b5..0c92bf687881 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/opa-component.adoc
@@ -308,6 +308,28 @@ one element failed, nor allowed because most of it 
succeeded. A call that fails
 reached at all - fails the exchange rather than denying each element: it 
carries no verdict at all, not a list of
 `false`. Under `failOpen` a call that could not reach the server allows every 
element instead.
 
+`CamelOpaBatchOutcomes` provides an immutable `List<OpaBatchOutcome>` in the 
same order, while
+`CamelOpaBatchDecision` keeps its existing Boolean-list contract. Each outcome 
has:
+
+[cols="1,3",options="header"]
+|===
+|Property |Meaning
+|`allowed` |Whether the element may proceed, including an explicit fail-open 
result.
+|`decision` |The normalized Boolean policy verdict, or `null` when no decision 
was obtained. This is not the raw policy document.
+|`failureCategory` |`NONE`, `UNAVAILABLE`, `EVALUATION_ERROR`, 
`MISSING_RESULT` or `INTERRUPTED`.
+|`failedOpen` |`true` only when the element proceeded because its decision 
point was unavailable and `failOpen` was enabled.
+|===
+
+These outcomes contain no exception objects or failure messages. A genuine 
policy denial has `decision=false`,
+`failureCategory=NONE` and `failedOpen=false`. An unavailable element allowed 
under `failOpen` has
+`allowed=true`, `decision=null`, `failureCategory=UNAVAILABLE` and 
`failedOpen=true`.
+`CamelOpaDecisionFailedOpen=true` marks a batch whenever *any* element failed 
open; inspect the outcomes to identify
+which ones. A call that could not reach the server at all is reported like an 
element whose own decision point was
+unavailable: under `failOpen` every element is `UNAVAILABLE` and 
`failedOpen=true`, and the outcomes do not say which
+of the two happened, since the route proceeds the same way in both cases. 
Empty batches return empty verdict and outcome lists. A whole-batch failure 
that fails closed carries
+neither list. The outcome header and aggregate marker are cleared before every 
evaluation, including a subsequent
+single-element evaluation.
+
 `batch` requires `evaluationMode=rest`: it saves the per-element HTTP 
round-trip, which has no meaning for in-process
 `wasm` evaluation, and the endpoint rejects the combination at startup.
 
diff --git 
a/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
 
b/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
index 6d841fa2a489..7eec7e2fdf00 100644
--- 
a/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
+++ 
b/components/camel-opa/src/generated/resources/META-INF/org/apache/camel/component/opa/opa.json
@@ -52,8 +52,9 @@
     "CamelOpaDecisionAllow": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "autowired": false, "secret": false, 
"description": "The allow\/deny verdict of the policy evaluation. Always 
overwritten by the component, so a value set by an inbound message never 
survives into the route.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION_ALLOW" },
     "CamelOpaDecision": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Object", "deprecated": false, "autowired": false, "secret": false, 
"description": "The raw decision document returned by OPA. Useful for policies 
that return more than a boolean, such as obligations, row filters or deny 
reasons.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#DECISION" },
     "CamelOpaPolicyPath": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "autowired": false, "secret": false, 
"description": "The policy path that was evaluated. Set by the component for 
observability; it is not read as an input and cannot be used to select a 
different policy.", "constantName": 
"org.apache.camel.component.opa.OpaConstants#POLICY_PATH" },
-    "CamelOpaDecisionFailedOpen": { "index": 3, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "autowired": false, "secret": 
false, "description": "Set to true only when the exchange proceeded because 
failOpen is enabled and the policy could not be evaluated - nothing authorized 
it. Absent on every decision an actual policy made, so a route or an audit 
trail can tell the two apart rather than s [...]
-    "CamelOpaBatchDecision": { "index": 4, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"java.util.List<Boolean>", "deprecated": false, "autowired": false, "secret": 
false, "description": "The per-element allow\/deny verdicts of a batch 
evaluation (batch=true), as a List of Boolean parallel to the List body. Always 
overwritten by the component. An element whose evaluation could not be reached 
is denied, unless failOpen is s [...]
+    "CamelOpaDecisionFailedOpen": { "index": 3, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "autowired": false, "secret": 
false, "description": "Set to true only when the exchange proceeded because 
failOpen is enabled and the policy could not be evaluated - nothing authorized 
it. Absent on every decision an actual policy made, so a route or an audit 
trail can tell the two apart rather than s [...]
+    "CamelOpaBatchDecision": { "index": 4, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"java.util.List<Boolean>", "deprecated": false, "autowired": false, "secret": 
false, "description": "The per-element allow\/deny verdicts of a batch 
evaluation (batch=true), as a List of Boolean parallel to the List body. Always 
overwritten by the component. An element whose evaluation could not be reached 
is denied, unless failOpen is s [...]
+    "CamelOpaBatchOutcomes": { "index": 5, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"java.util.List<org.apache.camel.component.opa.OpaBatchOutcome>", "deprecated": 
false, "autowired": false, "secret": false, "description": "Immutable 
per-element outcomes of a batch evaluation, parallel to the List body. Each 
outcome exposes the effective allowed verdict, nullable Boolean policy 
decision, failure category and failedOpen  [...]
   },
   "bodyTypes": {
     "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "batch", 
"value": "true", "carriers": [ "list" ] } ], "description": "Not read unless 
includeBody=true: then a String, number, boolean, Map or List goes into the 
input document as it is, and another body as its string form (a POJO as its 
toString). With batch=true a List, one input document per element: another body 
fails" },
diff --git a/components/camel-opa/src/main/docs/opa-component.adoc 
b/components/camel-opa/src/main/docs/opa-component.adoc
index 6c5f3077c2b5..0c92bf687881 100644
--- a/components/camel-opa/src/main/docs/opa-component.adoc
+++ b/components/camel-opa/src/main/docs/opa-component.adoc
@@ -308,6 +308,28 @@ one element failed, nor allowed because most of it 
succeeded. A call that fails
 reached at all - fails the exchange rather than denying each element: it 
carries no verdict at all, not a list of
 `false`. Under `failOpen` a call that could not reach the server allows every 
element instead.
 
+`CamelOpaBatchOutcomes` provides an immutable `List<OpaBatchOutcome>` in the 
same order, while
+`CamelOpaBatchDecision` keeps its existing Boolean-list contract. Each outcome 
has:
+
+[cols="1,3",options="header"]
+|===
+|Property |Meaning
+|`allowed` |Whether the element may proceed, including an explicit fail-open 
result.
+|`decision` |The normalized Boolean policy verdict, or `null` when no decision 
was obtained. This is not the raw policy document.
+|`failureCategory` |`NONE`, `UNAVAILABLE`, `EVALUATION_ERROR`, 
`MISSING_RESULT` or `INTERRUPTED`.
+|`failedOpen` |`true` only when the element proceeded because its decision 
point was unavailable and `failOpen` was enabled.
+|===
+
+These outcomes contain no exception objects or failure messages. A genuine 
policy denial has `decision=false`,
+`failureCategory=NONE` and `failedOpen=false`. An unavailable element allowed 
under `failOpen` has
+`allowed=true`, `decision=null`, `failureCategory=UNAVAILABLE` and 
`failedOpen=true`.
+`CamelOpaDecisionFailedOpen=true` marks a batch whenever *any* element failed 
open; inspect the outcomes to identify
+which ones. A call that could not reach the server at all is reported like an 
element whose own decision point was
+unavailable: under `failOpen` every element is `UNAVAILABLE` and 
`failedOpen=true`, and the outcomes do not say which
+of the two happened, since the route proceeds the same way in both cases. 
Empty batches return empty verdict and outcome lists. A whole-batch failure 
that fails closed carries
+neither list. The outcome header and aggregate marker are cleared before every 
evaluation, including a subsequent
+single-element evaluation.
+
 `batch` requires `evaluationMode=rest`: it saves the per-element HTTP 
round-trip, which has no meaning for in-process
 `wasm` evaluation, and the endpoint rejects the combination at startup.
 
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaBatchOutcome.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaBatchOutcome.java
new file mode 100644
index 000000000000..6952ebb4d09b
--- /dev/null
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaBatchOutcome.java
@@ -0,0 +1,86 @@
+/*
+ * 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.opa;
+
+import java.io.Serializable;
+
+/**
+ * An immutable batch element outcome, distinguishing a policy decision from a 
failure to obtain one.
+ * <p/>
+ * The decision is the normalized Boolean policy verdict, not the raw decision 
document. Failure messages and exceptions
+ * are deliberately omitted: they can contain request data or credentials.
+ * <p/>
+ * Serializable, so that the header holding the outcomes survives an 
aggregation repository that stores headers.
+ */
+public final class OpaBatchOutcome implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    public enum FailureCategory {
+        NONE,
+        UNAVAILABLE,
+        EVALUATION_ERROR,
+        MISSING_RESULT,
+        INTERRUPTED
+    }
+
+    private final boolean allowed;
+    private final Boolean decision;
+    private final FailureCategory failureCategory;
+    private final boolean failedOpen;
+
+    private OpaBatchOutcome(boolean allowed, Boolean decision, FailureCategory 
failureCategory, boolean failedOpen) {
+        this.allowed = allowed;
+        this.decision = decision;
+        this.failureCategory = failureCategory;
+        this.failedOpen = failedOpen;
+    }
+
+    static OpaBatchOutcome decision(boolean allowed) {
+        return new OpaBatchOutcome(allowed, allowed, FailureCategory.NONE, 
false);
+    }
+
+    static OpaBatchOutcome failure(FailureCategory category, boolean 
failedOpen) {
+        return new OpaBatchOutcome(failedOpen, null, category, failedOpen);
+    }
+
+    /** Whether this element is allowed to proceed, including an explicit 
fail-open result. */
+    public boolean isAllowed() {
+        return allowed;
+    }
+
+    /** The policy's normalized Boolean verdict, or null when no decision was 
obtained. */
+    public Boolean getDecision() {
+        return decision;
+    }
+
+    /** Why no decision was obtained, or NONE when a policy decision was 
obtained. */
+    public FailureCategory getFailureCategory() {
+        return failureCategory;
+    }
+
+    /** Whether the element proceeded only because failOpen was enabled and 
its decision point was unavailable. */
+    public boolean isFailedOpen() {
+        return failedOpen;
+    }
+
+    @Override
+    public String toString() {
+        return "OpaBatchOutcome[allowed=" + allowed + ", decision=" + decision 
+ ", failureCategory=" + failureCategory
+               + ", failedOpen=" + failedOpen + "]";
+    }
+}
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConstants.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConstants.java
index 583b0dd6f49e..eda410c66972 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConstants.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaConstants.java
@@ -43,7 +43,8 @@ public final class OpaConstants {
               description = "Set to true only when the exchange proceeded 
because failOpen is enabled and the policy"
                             + " could not be evaluated - nothing authorized 
it. Absent on every decision an actual"
                             + " policy made, so a route or an audit trail can 
tell the two apart rather than seeing"
-                            + " the same CamelOpaDecisionAllow=true for both.",
+                            + " the same CamelOpaDecisionAllow=true for both. 
For a batch, set when any element"
+                            + " proceeded because failOpen was enabled; 
CamelOpaBatchOutcomes identifies the elements.",
               javaType = "Boolean")
     public static final String DECISION_FAILED_OPEN = HEADER_PREFIX + 
"DecisionFailedOpen";
 
@@ -54,6 +55,13 @@ public final class OpaConstants {
               javaType = "java.util.List<Boolean>")
     public static final String BATCH_DECISION = HEADER_PREFIX + 
"BatchDecision";
 
+    @Metadata(label = "producer",
+              description = "Immutable per-element outcomes of a batch 
evaluation, parallel to the List body. Each"
+                            + " outcome exposes the effective allowed verdict, 
nullable Boolean policy decision,"
+                            + " failure category and failedOpen flag. Contains 
no exception or failure message.",
+              javaType = 
"java.util.List<org.apache.camel.component.opa.OpaBatchOutcome>")
+    public static final String BATCH_OUTCOMES = HEADER_PREFIX + 
"BatchOutcomes";
+
     private OpaConstants() {
     }
 }
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
index 18f4a66b6532..1e20ad88f96e 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaPolicyEvaluator.java
@@ -177,12 +177,15 @@ public abstract class OpaPolicyEvaluator {
 
     /**
      * Authorizes a list in one call and records the per-element verdicts in 
{@link OpaConstants#BATCH_DECISION}, a
-     * {@code List<Boolean>} parallel to the input.
+     * {@code List<Boolean>} parallel to the input, and the per-element {@link 
OpaBatchOutcome}s in
+     * {@link OpaConstants#BATCH_OUTCOMES}, which also say why an element has 
no decision.
+     * {@link OpaConstants#DECISION_FAILED_OPEN} is set when any element 
proceeded because it failed open.
      * <p/>
      * Fail-closed is per element: an element whose evaluation failed is 
denied while the others decide normally, and
      * under {@code failOpen} it is allowed only when its decision point was 
unavailable. Only a batch call that fails
      * as a whole denies every element - or, under {@code failOpen}, allows 
every element when the server could not be
-     * reached.
+     * reached. The outcomes report that case like an element whose own 
decision point was unavailable: every element is
+     * {@code UNAVAILABLE} and failed open; they do not say which of the two 
happened.
      */
     public List<Boolean> evaluateBatch(Exchange exchange, List<?> elements) 
throws OpaPolicyEvaluationException {
         clearDecisionHeaders(exchange);
@@ -191,7 +194,7 @@ public abstract class OpaPolicyEvaluator {
         // failure - and so, fail-closed, into an error thrown for an empty 
list. Answer it deterministically instead.
         if (elements.isEmpty()) {
             List<Boolean> verdicts = List.of();
-            setBatchDecisionHeaders(exchange, verdicts);
+            setBatchDecisionHeaders(exchange, verdicts, List.of());
             return verdicts;
         }
         Map<String, Map<String, Object>> inputs = new LinkedHashMap<>();
@@ -199,11 +202,11 @@ public abstract class OpaPolicyEvaluator {
             inputs.put(Integer.toString(i), buildInput(exchange, 
elements.get(i), true));
         }
 
-        List<Boolean> verdicts = new ArrayList<>(elements.size());
+        List<OpaBatchOutcome> outcomes = new ArrayList<>(elements.size());
         try {
             Map<String, BatchElement> results = evaluateBatchDecisions(inputs);
             for (int i = 0; i < elements.size(); i++) {
-                verdicts.add(verdictFor(i, results != null ? 
results.get(Integer.toString(i)) : null));
+                outcomes.add(outcomeFor(i, results != null ? 
results.get(Integer.toString(i)) : null));
             }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
@@ -220,32 +223,48 @@ public abstract class OpaPolicyEvaluator {
             LOG.warn("Batch policy {} could not be evaluated, allowing all {} 
elements because failOpen is enabled."
                      + " Reason: {}",
                     getPolicyPath(), elements.size(), e.getMessage());
+            outcomes.clear();
             for (int i = 0; i < elements.size(); i++) {
-                verdicts.add(Boolean.TRUE);
+                
outcomes.add(OpaBatchOutcome.failure(OpaBatchOutcome.FailureCategory.UNAVAILABLE,
 true));
             }
         }
 
-        setBatchDecisionHeaders(exchange, verdicts);
+        List<Boolean> verdicts = new ArrayList<>(outcomes.size());
+        for (OpaBatchOutcome outcome : outcomes) {
+            verdicts.add(outcome.isAllowed());
+        }
+        setBatchDecisionHeaders(exchange, verdicts, outcomes);
         return verdicts;
     }
 
-    private boolean verdictFor(int index, BatchElement element) {
-        if (element != null && element.succeeded()) {
-            return isAllowed(element.decision());
+    private OpaBatchOutcome outcomeFor(int index, BatchElement element) {
+        if (element == null) {
+            return 
OpaBatchOutcome.failure(OpaBatchOutcome.FailureCategory.MISSING_RESULT, false);
         }
-        if (element != null && element.failure() != null) {
-            restoreInterruptIfWrapped(element.failure());
+        if (element.succeeded()) {
+            return OpaBatchOutcome.decision(isAllowed(element.decision()));
         }
-        // a single element failed: deny it without failing the whole batch, 
unless failOpen is set and its decision
-        // point was unavailable
-        if (failOpen && element != null && element.failure() != null && 
isDecisionPointUnavailable(element.failure())) {
-            String reason = element.failure().getMessage();
+        Exception failure = element.failure();
+        restoreInterruptIfWrapped(failure);
+        OpaBatchOutcome.FailureCategory category = failureCategory(failure);
+        // Keep the same narrow fail-open scope as a single evaluation: a 
server that answered did not go away.
+        boolean failedOpen = failOpen && category == 
OpaBatchOutcome.FailureCategory.UNAVAILABLE;
+        if (failedOpen) {
             LOG.warn("Batch element {} of policy {} could not be evaluated, 
allowing it because failOpen is enabled."
                      + " Reason: {}",
-                    index, getPolicyPath(), reason);
-            return true;
+                    index, getPolicyPath(), failure.getMessage());
         }
-        return false;
+        return OpaBatchOutcome.failure(category, failedOpen);
+    }
+
+    private OpaBatchOutcome.FailureCategory failureCategory(Exception failure) 
{
+        for (Throwable cause = failure; cause != null; cause = 
cause.getCause()) {
+            if (cause instanceof InterruptedException) {
+                return OpaBatchOutcome.FailureCategory.INTERRUPTED;
+            }
+        }
+        return isDecisionPointUnavailable(failure)
+                ? OpaBatchOutcome.FailureCategory.UNAVAILABLE : 
OpaBatchOutcome.FailureCategory.EVALUATION_ERROR;
     }
 
     /**
@@ -393,6 +412,7 @@ public abstract class OpaPolicyEvaluator {
         // fail-open read as a decision a policy actually made
         message.removeHeader(OpaConstants.DECISION_FAILED_OPEN);
         message.removeHeader(OpaConstants.BATCH_DECISION);
+        message.removeHeader(OpaConstants.BATCH_OUTCOMES);
     }
 
     private void setDecisionHeaders(Exchange exchange, Object decision, 
boolean allowed) {
@@ -408,9 +428,13 @@ public abstract class OpaPolicyEvaluator {
      * As in {@link #evaluate}, a fail-closed batch failure never reaches 
here, leaving the exchange carrying no
      * verdict.
      */
-    private void setBatchDecisionHeaders(Exchange exchange, List<Boolean> 
verdicts) {
+    private void setBatchDecisionHeaders(Exchange exchange, List<Boolean> 
verdicts, List<OpaBatchOutcome> outcomes) {
         exchange.getMessage().setHeader(OpaConstants.BATCH_DECISION, verdicts);
+        exchange.getMessage().setHeader(OpaConstants.BATCH_OUTCOMES, 
List.copyOf(outcomes));
         exchange.getMessage().setHeader(OpaConstants.POLICY_PATH, policyPath);
+        if (outcomes.stream().anyMatch(OpaBatchOutcome::isFailedOpen)) {
+            exchange.getMessage().setHeader(OpaConstants.DECISION_FAILED_OPEN, 
true);
+        }
     }
 
     private static boolean isIncluded(Set<String> filter, String name) {
@@ -442,7 +466,8 @@ public abstract class OpaPolicyEvaluator {
                 || OpaConstants.DECISION.equalsIgnoreCase(name)
                 || OpaConstants.POLICY_PATH.equalsIgnoreCase(name)
                 || OpaConstants.DECISION_FAILED_OPEN.equalsIgnoreCase(name)
-                || OpaConstants.BATCH_DECISION.equalsIgnoreCase(name);
+                || OpaConstants.BATCH_DECISION.equalsIgnoreCase(name)
+                || OpaConstants.BATCH_OUTCOMES.equalsIgnoreCase(name);
     }
 
     /**
diff --git 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaRestEvaluator.java
 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaRestEvaluator.java
index 2a39c6e571e5..2116e6ae0270 100644
--- 
a/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaRestEvaluator.java
+++ 
b/components/camel-opa/src/main/java/org/apache/camel/component/opa/OpaRestEvaluator.java
@@ -130,10 +130,8 @@ public class OpaRestEvaluator extends OpaPolicyEvaluator 
implements AutoCloseabl
                 outcomes.put(entry.getKey(), new 
BatchElement(result.getValue(), null));
             } else {
                 Exception failure = result != null ? result.getException() : 
null;
-                if (failure == null) {
-                    failure = new IllegalStateException("no result returned 
for batch element " + entry.getKey());
-                }
-                outcomes.put(entry.getKey(), new BatchElement(null, failure));
+                // A missing result is different from an evaluation that 
reported a failure, even though both deny.
+                outcomes.put(entry.getKey(), failure != null ? new 
BatchElement(null, failure) : null);
             }
         }
         return outcomes;
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaBatchEvaluationTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaBatchEvaluationTest.java
index 10291002fbc9..1f5b29c3d561 100644
--- 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaBatchEvaluationTest.java
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaBatchEvaluationTest.java
@@ -67,6 +67,11 @@ public class OpaBatchEvaluationTest extends CamelTestSupport 
{
         when(client.evaluateBatch(eq(PATH), anyMap())).thenReturn(results);
     }
 
+    @SuppressWarnings("unchecked")
+    private static List<OpaBatchOutcome> outcomes(Exchange exchange) {
+        return exchange.getMessage().getHeader(OpaConstants.BATCH_OUTCOMES, 
List.class);
+    }
+
     @Test
     void reportsAVerdictParallelToEachElement() throws Exception {
         Map<String, OPAResult> results = new LinkedHashMap<>();
@@ -81,6 +86,9 @@ public class OpaBatchEvaluationTest extends CamelTestSupport {
         assertThat(out.getException()).isNull();
         assertThat(out.getMessage().getHeader(OpaConstants.BATCH_DECISION, 
List.class))
                 .containsExactly(true, false, true);
+        
assertThat(outcomes(out)).extracting(OpaBatchOutcome::getDecision).containsExactly(true,
 false, true);
+        
assertThat(outcomes(out)).extracting(OpaBatchOutcome::isFailedOpen).containsOnly(false);
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
         // batch mode must set the policy-path header too, so observability 
tooling reads the same
         // CamelOpaPolicyPath as it does after a single evaluation
         assertThat(out.getMessage().getHeader(OpaConstants.POLICY_PATH, 
String.class)).isEqualTo(PATH);
@@ -167,6 +175,8 @@ public class OpaBatchEvaluationTest extends 
CamelTestSupport {
 
         assertThat(out.getException()).isNull();
         assertThat(out.getMessage().getHeader(OpaConstants.BATCH_DECISION, 
List.class)).isEmpty();
+        assertThat(outcomes(out)).isEmpty();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
         assertThat(out.getMessage().getHeader(OpaConstants.POLICY_PATH, 
String.class)).isEqualTo(PATH);
         verify(client, never()).evaluateBatch(anyString(), anyMap());
     }
@@ -199,6 +209,11 @@ public class OpaBatchEvaluationTest extends 
CamelTestSupport {
         assertThat(out.getException()).isNull();
         assertThat(out.getMessage().getHeader(OpaConstants.BATCH_DECISION, 
List.class))
                 .containsExactly(true, true, true);
+        
assertThat(outcomes(out)).extracting(OpaBatchOutcome::getDecision).containsOnlyNulls();
+        
assertThat(outcomes(out)).extracting(OpaBatchOutcome::isFailedOpen).containsOnly(true);
+        
assertThat(outcomes(out)).extracting(OpaBatchOutcome::getFailureCategory)
+                .containsOnly(OpaBatchOutcome.FailureCategory.UNAVAILABLE);
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isEqualTo(true);
     }
 
     @Test
@@ -231,4 +246,91 @@ public class OpaBatchEvaluationTest extends 
CamelTestSupport {
         
assertThat(out.getException()).isInstanceOf(OpaPolicyEvaluationException.class);
         
assertThat(out.getMessage().getHeader(OpaConstants.BATCH_DECISION)).isNull();
     }
+
+    @Test
+    void distinguishesDecisionsFailuresAndMissingResults() throws Exception {
+        Map<String, OPAResult> results = new LinkedHashMap<>();
+        results.put("0", new OPAResult(Boolean.TRUE));
+        results.put("1", new OPAResult(Boolean.FALSE));
+        results.put("2", failed(unreachable(PATH)));
+        results.put("3", failed(evaluationError(PATH)));
+        // Index 4 is absent; index 5 exists but the SDK returned no result 
for it.
+        results.put("5", null);
+        stubBatch(results);
+
+        Exchange out = template.request("opa:" + PATH + 
"?opaClient=#opaClient&batch=true&failOpen=true",
+                e -> e.getMessage().setBody(List.of("a", "b", "c", "d", "e", 
"f")));
+
+        assertThat(out.getException()).isNull();
+        assertThat(out.getMessage().getHeader(OpaConstants.BATCH_DECISION, 
List.class))
+                .containsExactly(true, false, true, false, false, false);
+        List<OpaBatchOutcome> outcomes = outcomes(out);
+        assertThat(outcomes).extracting(OpaBatchOutcome::isAllowed)
+                .containsExactly(true, false, true, false, false, false);
+        assertThat(outcomes).extracting(OpaBatchOutcome::getDecision)
+                .containsExactly(true, false, null, null, null, null);
+        
assertThat(outcomes).extracting(OpaBatchOutcome::getFailureCategory).containsExactly(
+                OpaBatchOutcome.FailureCategory.NONE, 
OpaBatchOutcome.FailureCategory.NONE,
+                OpaBatchOutcome.FailureCategory.UNAVAILABLE, 
OpaBatchOutcome.FailureCategory.EVALUATION_ERROR,
+                OpaBatchOutcome.FailureCategory.MISSING_RESULT, 
OpaBatchOutcome.FailureCategory.MISSING_RESULT);
+        assertThat(outcomes).extracting(OpaBatchOutcome::isFailedOpen)
+                .containsExactly(false, false, true, false, false, false);
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isEqualTo(true);
+        assertThatThrownBy(() -> 
outcomes.add(outcomes.get(0))).isInstanceOf(UnsupportedOperationException.class);
+    }
+
+    @Test
+    void identifiesAnInterruptedElementWithoutFailingOpen() throws Exception {
+        Map<String, OPAResult> results = new LinkedHashMap<>();
+        results.put("0", failed(interrupted(PATH)));
+        stubBatch(results);
+        try {
+            Exchange out = template.request("opa:" + PATH + 
"?opaClient=#opaClient&batch=true&failOpen=true",
+                    e -> e.getMessage().setBody(List.of("a")));
+
+            assertThat(Thread.currentThread().isInterrupted()).isTrue();
+            assertThat(out.getException()).isNull();
+            
assertThat(outcomes(out)).extracting(OpaBatchOutcome::getFailureCategory)
+                    
.containsExactly(OpaBatchOutcome.FailureCategory.INTERRUPTED);
+            
assertThat(outcomes(out)).extracting(OpaBatchOutcome::isAllowed).containsExactly(false);
+            
assertThat(outcomes(out)).extracting(OpaBatchOutcome::isFailedOpen).containsExactly(false);
+            
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
+        } finally {
+            Thread.interrupted();
+        }
+    }
+
+    @Test
+    void clearsOldDiagnosticsBeforeAWholeBatchFailure() throws Exception {
+        when(client.evaluateBatch(eq(PATH), 
anyMap())).thenThrow(unreachable(PATH));
+
+        Exchange out = template.request("opa:" + PATH + 
"?opaClient=#opaClient&batch=true", e -> {
+            e.getMessage().setBody(List.of("a"));
+            e.getMessage().setHeader(OpaConstants.BATCH_OUTCOMES, List.of("old 
outcome"));
+            e.getMessage().setHeader(OpaConstants.DECISION_FAILED_OPEN, true);
+        });
+
+        
assertThat(out.getException()).isInstanceOf(OpaPolicyEvaluationException.class);
+        assertThat(outcomes(out)).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.BATCH_DECISION)).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
+    }
+
+    @Test
+    void clearsBatchDiagnosticsBeforeASubsequentSingleDecision() throws 
Exception {
+        stubBatch(Map.of("0", failed(unreachable(PATH))));
+        when(client.evaluate(eq(PATH), anyMap(), 
eq(Object.class))).thenReturn(Boolean.TRUE);
+        Exchange out = template.request("opa:" + PATH + 
"?opaClient=#opaClient&batch=true&failOpen=true",
+                e -> e.getMessage().setBody(List.of("a")));
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isEqualTo(true);
+
+        template.send("opa:" + PATH + "?opaClient=#opaClient", out);
+
+        assertThat(out.getException()).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_ALLOW)).isEqualTo(true);
+        assertThat(outcomes(out)).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.BATCH_DECISION)).isNull();
+        
assertThat(out.getMessage().getHeader(OpaConstants.DECISION_FAILED_OPEN)).isNull();
+    }
+
 }
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaBatchOutcomeTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaBatchOutcomeTest.java
new file mode 100644
index 000000000000..f581327b9713
--- /dev/null
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaBatchOutcomeTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.opa;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OpaBatchOutcomeTest {
+
+    @Test
+    void describesTheSanitizedDecisionAndFailureState() {
+        assertThat(OpaBatchOutcome.decision(false).toString())
+                .isEqualTo("OpaBatchOutcome[allowed=false, decision=false, 
failureCategory=NONE, failedOpen=false]");
+        
assertThat(OpaBatchOutcome.failure(OpaBatchOutcome.FailureCategory.UNAVAILABLE, 
true).toString())
+                .isEqualTo("OpaBatchOutcome[allowed=true, decision=null, 
failureCategory=UNAVAILABLE, failedOpen=true]");
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void preservesTheBatchHeaderListThroughJavaSerialization() throws 
Exception {
+        List<OpaBatchOutcome> header = List.copyOf(List.of(
+                OpaBatchOutcome.decision(true),
+                OpaBatchOutcome.decision(false),
+                
OpaBatchOutcome.failure(OpaBatchOutcome.FailureCategory.UNAVAILABLE, true),
+                
OpaBatchOutcome.failure(OpaBatchOutcome.FailureCategory.EVALUATION_ERROR, 
false)));
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
+            output.writeObject(header);
+        }
+
+        List<OpaBatchOutcome> restored;
+        try (ObjectInputStream input = new ObjectInputStream(new 
ByteArrayInputStream(bytes.toByteArray()))) {
+            restored = (List<OpaBatchOutcome>) input.readObject();
+        }
+
+        
assertThat(restored).extracting(OpaBatchOutcome::isAllowed).containsExactly(true,
 false, true, false);
+        
assertThat(restored).extracting(OpaBatchOutcome::getDecision).containsExactly(true,
 false, null, null);
+        
assertThat(restored).extracting(OpaBatchOutcome::getFailureCategory).containsExactly(
+                OpaBatchOutcome.FailureCategory.NONE, 
OpaBatchOutcome.FailureCategory.NONE,
+                OpaBatchOutcome.FailureCategory.UNAVAILABLE, 
OpaBatchOutcome.FailureCategory.EVALUATION_ERROR);
+        
assertThat(restored).extracting(OpaBatchOutcome::isFailedOpen).containsExactly(false,
 false, true, false);
+    }
+}
diff --git 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
index fa06c74a1321..1046286c56b1 100644
--- 
a/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
+++ 
b/components/camel-opa/src/test/java/org/apache/camel/component/opa/OpaInputDocumentTest.java
@@ -83,11 +83,12 @@ class OpaInputDocumentTest extends CamelTestSupport {
             e.getMessage().setHeader(OpaConstants.DECISION_ALLOW, true);
             e.getMessage().setHeader(OpaConstants.DECISION, Map.of("allow", 
true));
             e.getMessage().setHeader(OpaConstants.POLICY_PATH, 
"authz/some-other-policy");
+            e.getMessage().setHeader(OpaConstants.BATCH_OUTCOMES, "claimed 
outcome");
         });
 
         assertThat(headersOf(input)).containsEntry("user", "alice");
         assertThat(headersOf(input)).doesNotContainKeys(
-                OpaConstants.DECISION_ALLOW, OpaConstants.DECISION, 
OpaConstants.POLICY_PATH);
+                OpaConstants.DECISION_ALLOW, OpaConstants.DECISION, 
OpaConstants.POLICY_PATH, OpaConstants.BATCH_OUTCOMES);
     }
 
     @Test
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 91fe68dc137b..01eea679d7f5 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -13,6 +13,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 
 == Upgrading Camel 4.23 to 4.24
 
+=== camel-opa - batch fail-open marker
+
+Batch evaluation now sets `CamelOpaDecisionFailedOpen=true` when any element 
proceeds because its decision point
+was unavailable and `failOpen=true`. Previously this marker was set only for 
single-element evaluation. Routes
+that inspect the marker after a batch should account for it applying to any 
element; the per-element outcome
+list identifies the affected elements. The existing `CamelOpaBatchDecision` 
Boolean list is unchanged.
+
 === camel-observability-services - promoted to Stable
 
 The `camel-observability-services` component has been promoted from _Preview_ 
to _Stable_ support level.
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
index 55eaf1dd52b1..0f2697bd44aa 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpaEndpointBuilderFactory.java
@@ -736,7 +736,9 @@ public interface OpaEndpointBuilderFactory {
          * enabled and the policy could not be evaluated - nothing authorized
          * it. Absent on every decision an actual policy made, so a route or an
          * audit trail can tell the two apart rather than seeing the same
-         * CamelOpaDecisionAllow=true for both.
+         * CamelOpaDecisionAllow=true for both. For a batch, set when any
+         * element proceeded because failOpen was enabled; 
CamelOpaBatchOutcomes
+         * identifies the elements.
          * 
          * The option is a: {@code Boolean} type.
          * 
@@ -762,6 +764,22 @@ public interface OpaEndpointBuilderFactory {
         public String opaBatchDecision() {
             return "CamelOpaBatchDecision";
         }
+        /**
+         * Immutable per-element outcomes of a batch evaluation, parallel to 
the
+         * List body. Each outcome exposes the effective allowed verdict,
+         * nullable Boolean policy decision, failure category and failedOpen
+         * flag. Contains no exception or failure message.
+         * 
+         * The option is a: {@code
+         * java.util.List<org.apache.camel.component.opa.OpaBatchOutcome>} 
type.
+         * 
+         * Group: producer
+         * 
+         * @return the name of the header {@code OpaBatchOutcomes}.
+         */
+        public String opaBatchOutcomes() {
+            return "CamelOpaBatchOutcomes";
+        }
     }
     static OpaEndpointBuilder endpointBuilder(String componentName, String 
path) {
         class OpaEndpointBuilderImpl extends AbstractEndpointBuilder 
implements OpaEndpointBuilder, AdvancedOpaEndpointBuilder {

Reply via email to