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 64850880b504 CAMEL-23949: Fail fast when ADD is missing embedding
header
64850880b504 is described below
commit 64850880b50475d47f3438cb53b0bd9ae54fb878
Author: Omar Atie <[email protected]>
AuthorDate: Sun Jul 19 23:43:19 2026 -0700
CAMEL-23949: Fail fast when ADD is missing embedding header
Throw NoSuchHeaderException instead of calling EmbeddingStore.add(null)
when the CamelLangChain4jEmbeddingsEmbedding header is absent on ADD
operations in camel-langchain4j-embeddingstore.
Co-Authored-By: Cursor <[email protected]>
---
.../LangChain4jEmbeddingStoreProducer.java | 14 +-
...4jEmbeddingStoreMissingEmbeddingHeaderTest.java | 202 +++++++++++++++++++++
2 files changed, 210 insertions(+), 6 deletions(-)
diff --git
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreProducer.java
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreProducer.java
index 7a5a61c183cb..257583509ed9 100644
---
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreProducer.java
+++
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreProducer.java
@@ -128,16 +128,18 @@ public class LangChain4jEmbeddingStoreProducer extends
DefaultProducer {
*/
private void add(Exchange exchange) throws Exception {
final Message in = exchange.getMessage();
- Embedding embedding = null;
- TextSegment text = null;
- String id = null;
- if (in.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING) != null) {
- embedding = in.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING,
Embedding.class);
+ if (in.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING) == null) {
+ throw new NoSuchHeaderException(
+ "The embedding is a required header for ADD operations",
exchange,
+ LangChain4jEmbeddingsHeaders.EMBEDDING);
}
+ Embedding embedding =
in.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING, Embedding.class);
+ String id;
+
if (in.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT) != null) {
- text = in.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT,
TextSegment.class);
+ TextSegment text =
in.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT, TextSegment.class);
id =
getEndpoint().getConfiguration().getEmbeddingStore().add(embedding, text);
} else {
id =
getEndpoint().getConfiguration().getEmbeddingStore().add(embedding);
diff --git
a/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreMissingEmbeddingHeaderTest.java
b/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreMissingEmbeddingHeaderTest.java
new file mode 100644
index 000000000000..8f4bc5de91e6
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreMissingEmbeddingHeaderTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.langchain4j.embeddingstore;
+
+import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.NoSuchHeaderException;
+import
org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class LangChain4jEmbeddingStoreMissingEmbeddingHeaderTest extends
CamelTestSupport {
+
+ private RecordingEmbeddingStore embeddingStore;
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ embeddingStore = new RecordingEmbeddingStore();
+
+ LangChain4jEmbeddingStoreComponent component = context.getComponent(
+ LangChain4jEmbeddingStore.SCHEME,
LangChain4jEmbeddingStoreComponent.class);
+ component.getConfiguration().setEmbeddingStore(embeddingStore);
+
+ return context;
+ }
+
+ @Test
+ @DisplayName("ADD without embedding header fails fast with
NoSuchHeaderException")
+ void addWithoutEmbeddingHeaderFailsFast() {
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .request(Exchange.class);
+
+ assertMissingEmbeddingHeader(result);
+ assertThat(embeddingStore.getAddInvocations()).isZero();
+ }
+
+ @Test
+ @DisplayName("ADD with endpoint default action but no embedding header
fails fast")
+ void addWithEndpointDefaultActionAndMissingEmbeddingHeaderFailsFast() {
+ Exchange result =
fluentTemplate.to("langchain4j-embeddingstore:test?action=ADD")
+ .request(Exchange.class);
+
+ assertMissingEmbeddingHeader(result);
+ assertThat(embeddingStore.getAddInvocations()).isZero();
+ }
+
+ @Test
+ @DisplayName("ADD with text segment but no embedding header still fails
fast")
+ void addWithTextSegmentButNoEmbeddingHeaderFailsFast() {
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT,
TextSegment.from("hello"))
+ .request(Exchange.class);
+
+ assertMissingEmbeddingHeader(result);
+ assertThat(embeddingStore.getAddInvocations()).isZero();
+ }
+
+ @Test
+ @DisplayName("ADD with embedding header stores the embedding")
+ void addWithEmbeddingHeaderSucceeds() {
+ Embedding embedding = Embedding.from(new float[] { 0.1f, 0.2f, 0.3f });
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDING, embedding)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(result.getMessage().getBody(String.class)).isNotBlank();
+ assertThat(embeddingStore.getAddInvocations()).isEqualTo(1);
+ assertThat(embeddingStore.getLastEmbedding()).isSameAs(embedding);
+ assertThat(embeddingStore.getLastTextSegment()).isNull();
+ }
+
+ @Test
+ @DisplayName("ADD with embedding and text segment stores both values")
+ void addWithEmbeddingAndTextSegmentSucceeds() {
+ Embedding embedding = Embedding.from(new float[] { 0.4f, 0.5f });
+ TextSegment textSegment = TextSegment.from("segment");
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDING, embedding)
+ .withHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT,
textSegment)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(embeddingStore.getAddInvocations()).isEqualTo(1);
+ assertThat(embeddingStore.getLastEmbedding()).isSameAs(embedding);
+ assertThat(embeddingStore.getLastTextSegment()).isSameAs(textSegment);
+ }
+
+ @Test
+ @DisplayName("Missing action header is still rejected")
+ void missingActionHeaderFailsFast() {
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDING,
Embedding.from(new float[] { 1.0f }))
+ .request(Exchange.class);
+
+
assertThat(result.getException()).isInstanceOf(NoSuchHeaderException.class);
+ assertThat(((NoSuchHeaderException)
result.getException()).getHeaderName())
+ .isEqualTo(LangChain4jEmbeddingStoreHeaders.ACTION);
+ assertThat(embeddingStore.getAddInvocations()).isZero();
+ }
+
+ @Test
+ @DisplayName("REMOVE does not require the embedding header")
+ void removeWithoutEmbeddingHeaderSucceeds() {
+ String id = embeddingStore.add(Embedding.from(new float[] { 0.9f }));
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.REMOVE)
+ .withBody(id)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(embeddingStore.getRemoveInvocations()).isEqualTo(1);
+ assertThat(embeddingStore.getLastRemovedId()).isEqualTo(id);
+ }
+
+ private void assertMissingEmbeddingHeader(Exchange result) {
+
assertThat(result.getException()).isInstanceOf(NoSuchHeaderException.class);
+ NoSuchHeaderException exception = (NoSuchHeaderException)
result.getException();
+
assertThat(exception.getHeaderName()).isEqualTo(LangChain4jEmbeddingsHeaders.EMBEDDING);
+ assertThat(exception.getMessage()).contains("required header");
+ }
+
+ private static final class RecordingEmbeddingStore extends
InMemoryEmbeddingStore<TextSegment> {
+
+ private int addInvocations;
+ private int removeInvocations;
+ private Embedding lastEmbedding;
+ private TextSegment lastTextSegment;
+ private String lastRemovedId;
+
+ @Override
+ public String add(Embedding embedding) {
+ addInvocations++;
+ lastEmbedding = embedding;
+ lastTextSegment = null;
+ return super.add(embedding);
+ }
+
+ @Override
+ public String add(Embedding embedding, TextSegment embedded) {
+ addInvocations++;
+ lastEmbedding = embedding;
+ lastTextSegment = embedded;
+ return super.add(embedding, embedded);
+ }
+
+ @Override
+ public void remove(String id) {
+ removeInvocations++;
+ lastRemovedId = id;
+ super.remove(id);
+ }
+
+ int getAddInvocations() {
+ return addInvocations;
+ }
+
+ int getRemoveInvocations() {
+ return removeInvocations;
+ }
+
+ Embedding getLastEmbedding() {
+ return lastEmbedding;
+ }
+
+ TextSegment getLastTextSegment() {
+ return lastTextSegment;
+ }
+
+ String getLastRemovedId() {
+ return lastRemovedId;
+ }
+ }
+}