This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-26283 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 57d442a79d74d33c1283cc046f480d2056a99db5 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Nov 12 17:17:35 2026 +0100 CAMEL-26283: camel-elasticsearch-rest-client - send the document id and the index name as one encoded path segment The producer put the index name and the document id into the request path as they were, and the low-level client parses that path as a URI. A '/', '?' or '#' in the value changed the path of the request, and a space or '%' made it fail. Both values are now percent-encoded as one path segment each. A document id of '.' or '..' is rejected, and an index name is checked against the characters Elasticsearch does not allow in index names; a comma separated list of names and patterns still works. GET_BY_ID returns no document when the response has no _source, instead of failing with a NullPointerException. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../docs/elasticsearch-rest-client-component.adoc | 6 + .../docs/elasticsearch-rest-client-component.adoc | 6 + .../client/ElasticsearchRestClientProducer.java | 59 +++++- .../ElasticsearchRestClientRequestPathTest.java | 212 +++++++++++++++++++++ .../ElasticsearchRestClientComponentIT.java | 37 ++++ .../ElasticsearchRestClientITSupport.java | 11 +- .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc | 12 ++ .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 12 ++ .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 12 ++ 9 files changed, 356 insertions(+), 11 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/elasticsearch-rest-client-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/elasticsearch-rest-client-component.adoc index 2f920a8bce15..398a7f701b39 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/elasticsearch-rest-client-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/elasticsearch-rest-client-component.adoc @@ -74,6 +74,12 @@ You can set the message exchange body to a `Map` of `String` keys & values for t |=== +The document id and the index name are each sent as one path segment of the request, with reserved characters such as +`/`, `?`, `#`, `%` and spaces percent-encoded, so Elasticsearch takes them literally. A document id of `.` or `..` is +rejected, as is an index name that contains a character Elasticsearch does not allow in an index name (`\`, `/`, `?`, +`"`, `<`, `>`, `|`, `#` or a space). A comma separated list of index names and patterns, such as `logs-*,my_index`, can +be used to search several indexes. + == Examples === Index Content Example diff --git a/components/camel-elasticsearch-rest-client/src/main/docs/elasticsearch-rest-client-component.adoc b/components/camel-elasticsearch-rest-client/src/main/docs/elasticsearch-rest-client-component.adoc index 2f920a8bce15..398a7f701b39 100644 --- a/components/camel-elasticsearch-rest-client/src/main/docs/elasticsearch-rest-client-component.adoc +++ b/components/camel-elasticsearch-rest-client/src/main/docs/elasticsearch-rest-client-component.adoc @@ -74,6 +74,12 @@ You can set the message exchange body to a `Map` of `String` keys & values for t |=== +The document id and the index name are each sent as one path segment of the request, with reserved characters such as +`/`, `?`, `#`, `%` and spaces percent-encoded, so Elasticsearch takes them literally. A document id of `.` or `..` is +rejected, as is an index name that contains a character Elasticsearch does not allow in an index name (`\`, `/`, `?`, +`"`, `<`, `>`, `|`, `#` or a space). A comma separated list of index names and patterns, such as `logs-*,my_index`, can +be used to search several indexes. + == Examples === Index Content Example diff --git a/components/camel-elasticsearch-rest-client/src/main/java/org/apache/camel/component/elasticsearch/rest/client/ElasticsearchRestClientProducer.java b/components/camel-elasticsearch-rest-client/src/main/java/org/apache/camel/component/elasticsearch/rest/client/ElasticsearchRestClientProducer.java index b45572f45b33..367e8cf00b26 100644 --- a/components/camel-elasticsearch-rest-client/src/main/java/org/apache/camel/component/elasticsearch/rest/client/ElasticsearchRestClientProducer.java +++ b/components/camel-elasticsearch-rest-client/src/main/java/org/apache/camel/component/elasticsearch/rest/client/ElasticsearchRestClientProducer.java @@ -17,6 +17,8 @@ package org.apache.camel.component.elasticsearch.rest.client; import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; @@ -68,6 +70,9 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { public static final String POST = "POST"; public static final String GET = "GET"; + // the characters that an index name must not contain, besides the comma and the wildcard of a list or pattern + private static final String INVALID_INDEX_NAME_CHARS = "\\/?\"<>|# "; + private ElasticsearchRestClientEndpoint endpoint; private RestClient restClient; private boolean createdRestClient; @@ -205,7 +210,7 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { * Generate the Request for operation CREATE_INDEX */ private Request createIndexRequest(String indexName, Exchange exchange) { - var endpoint = String.format("/%s", indexName); + var endpoint = "/" + indexPath(indexName); var request = new Request(PUT, endpoint); var additionalParameters = exchange.getMessage().getHeader(ElasticSearchRestClientConstant.INDEX_SETTINGS, String.class); @@ -219,7 +224,7 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { * Generate the Request for operation DELETE_INDEX */ private Request deleteIndexRequest(String indexName) { - var endpoint = String.format("/%s", indexName); + var endpoint = "/" + indexPath(indexName); return new Request(DELETE, endpoint); } @@ -229,12 +234,12 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { private Request indexRequest(String indexName, Exchange exchange) throws InvalidPayloadException { var jsonBody = exchange.getMessage().getMandatoryBody(String.class); - var endpoint = String.format("/%s/_doc", indexName); + var endpoint = "/" + indexPath(indexName) + "/_doc"; // Gets the id of the document if specified - var id = exchange.getMessage().getHeader(ElasticSearchRestClientConstant.ID); + var id = exchange.getMessage().getHeader(ElasticSearchRestClientConstant.ID, String.class); if (ObjectHelper.isNotEmpty(id)) { - endpoint = String.format("%s/%s", endpoint, id); + endpoint = endpoint + "/" + documentIdPath(id); } Request request = new Request(POST, endpoint); @@ -256,7 +261,7 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { "id value is mandatory when performing GET_BY_ID operation"); } } - var endpoint = String.format("/%s/_doc/%s", indexName, id); + var endpoint = "/" + indexPath(indexName) + "/_doc/" + documentIdPath(id); return new Request(GET, endpoint); } @@ -273,7 +278,7 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { "id value is mandatory when performing DELETE operation"); } } - var endpoint = String.format("/%s/_doc/%s", indexName, id); + var endpoint = "/" + indexPath(indexName) + "/_doc/" + documentIdPath(id); return new Request(DELETE, endpoint); } @@ -281,7 +286,7 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { * Generate the Request for operation SEARCH */ private Request search(String indexName, Exchange exchange) { - var endpoint = String.format("/%s/_search", indexName); + var endpoint = "/" + indexPath(indexName) + "/_search"; // Create Request : at this stage this one get all results Request request = new Request(GET, endpoint); @@ -307,6 +312,40 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { return request; } + /** + * The index name, or the comma separated list of index names, as one path segment of the request. Each name is + * checked against the characters Elasticsearch does not allow in an index name, except the wildcard ({@code *}) of + * an index pattern. + */ + private static String indexPath(String indexName) { + for (String name : indexName.split(",", -1)) { + if (name.isEmpty() || ".".equals(name) || "..".equals(name) + || name.chars().anyMatch(c -> INVALID_INDEX_NAME_CHARS.indexOf(c) >= 0)) { + throw new IllegalArgumentException( + "Invalid index name: " + indexName + " (an index name must not be empty, . or .., and must not contain" + + " \\ / ? \" < > | # or a space)"); + } + } + return pathSegment(indexName); + } + + /** + * The document id as one path segment of the request + */ + private static String documentIdPath(String id) { + if (".".equals(id) || "..".equals(id)) { + throw new IllegalArgumentException("Invalid document id: " + id); + } + return pathSegment(id); + } + + /** + * Encodes the value, so it is sent as one path segment, with the reserved characters percent-encoded + */ + private static String pathSegment(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20"); + } + /** * creates an Elasticsearch Json Query based on matches */ @@ -357,6 +396,10 @@ public class ElasticsearchRestClientProducer extends DefaultAsyncProducer { String extractDocument(JsonObject doc) { // the document exists: for a missing document the server answers 404, which fails the request before Map<String, Object> map = doc.getMap("_source"); + if (map == null) { + // a response without the source of the document + return null; + } JsonObject jsonObject = new JsonObject(map); return jsonObject.toJson(); diff --git a/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/ElasticsearchRestClientRequestPathTest.java b/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/ElasticsearchRestClientRequestPathTest.java new file mode 100644 index 000000000000..6faf19e3bf65 --- /dev/null +++ b/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/ElasticsearchRestClientRequestPathTest.java @@ -0,0 +1,212 @@ +/* + * 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.elasticsearch.rest.client; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Stream; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The path of the request the producer sends: the document id and the index name are each one path segment, checked + * against a node that records the raw request and answers it as Elasticsearch does. + */ +class ElasticsearchRestClientRequestPathTest { + + private static final String DOCUMENT_ID = "a/b?c#d e%"; + private static final String ENCODED_DOCUMENT_ID = "a%2Fb%3Fc%23d%20e%25"; + + private final List<Recorded> requests = new CopyOnWriteArrayList<>(); + private HttpServer node; + private CamelContext context; + private ProducerTemplate template; + private String uri; + + @BeforeEach + void start() throws Exception { + node = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + node.createContext("/", this::answer); + node.start(); + uri = "elasticsearch-rest-client:camel?hostAddressesList=127.0.0.1:" + node.getAddress().getPort(); + context = new DefaultCamelContext(); + context.start(); + template = context.createProducerTemplate(); + } + + @AfterEach + void stop() { + context.stop(); + node.stop(0); + } + + static Stream<Arguments> documentIds() { + return Stream.of( + Arguments.of("a/b", "a%2Fb"), + Arguments.of("1?x=y", "1%3Fx%3Dy"), + Arguments.of("1#2", "1%232"), + Arguments.of("a b", "a%20b"), + Arguments.of("100%", "100%25"), + Arguments.of("café", "caf%C3%A9"), + Arguments.of(DOCUMENT_ID, ENCODED_DOCUMENT_ID)); + } + + @ParameterizedTest + @MethodSource("documentIds") + void getByIdSendsTheIdAsOnePathSegment(String id, String encoded) { + String document = template.requestBodyAndHeader(uri + "&operation=GET_BY_ID&indexName=my_index", null, + ElasticSearchRestClientConstant.ID, id, String.class); + + assertEquals("{\"title\":\"camel\"}", document); + assertEquals("/my_index/_doc/" + encoded, single().rawPath()); + assertNull(single().rawQuery()); + } + + @Test + void getByIdSendsTheIdOfTheBodyAsOnePathSegment() { + template.requestBody(uri + "&operation=GET_BY_ID&indexName=my_index", DOCUMENT_ID, String.class); + + assertEquals("/my_index/_doc/" + ENCODED_DOCUMENT_ID, single().rawPath()); + assertNull(single().rawQuery()); + } + + @Test + void indexSendsTheIdAsOnePathSegment() { + String id = template.requestBodyAndHeader(uri + "&operation=INDEX_OR_UPDATE&indexName=my_index", + "{\"title\":\"camel\"}", ElasticSearchRestClientConstant.ID, DOCUMENT_ID, String.class); + + assertEquals("1", id); + assertEquals("POST", single().method()); + assertEquals("/my_index/_doc/" + ENCODED_DOCUMENT_ID, single().rawPath()); + assertNull(single().rawQuery()); + } + + @Test + void indexWithoutIdStillLetsTheServerChooseTheId() { + template.requestBody(uri + "&operation=INDEX_OR_UPDATE&indexName=my_index", "{\"title\":\"camel\"}", String.class); + + assertEquals("/my_index/_doc", single().rawPath()); + } + + @Test + void deleteSendsTheIdAsOnePathSegment() { + Object deleted = template.requestBodyAndHeader(uri + "&operation=DELETE&indexName=my_index", null, + ElasticSearchRestClientConstant.ID, DOCUMENT_ID); + + assertEquals(Boolean.TRUE, deleted); + assertEquals("DELETE", single().method()); + assertEquals("/my_index/_doc/" + ENCODED_DOCUMENT_ID, single().rawPath()); + assertNull(single().rawQuery()); + } + + @ParameterizedTest + @ValueSource(strings = { ".", ".." }) + void dotSegmentDocumentIdIsRejected(String id) { + Exchange exchange = template.request(uri + "&operation=GET_BY_ID&indexName=my_index", + e -> e.getMessage().setHeader(ElasticSearchRestClientConstant.ID, id)); + + assertInstanceOf(IllegalArgumentException.class, exchange.getException()); + assertTrue(requests.isEmpty(), "requests sent: " + requests); + } + + @Test + void indexNamesAndPatternsOfASearchAreSentAsOnePathSegment() { + template.requestBodyAndHeader(uri + "&operation=SEARCH", null, ElasticSearchRestClientConstant.INDEX_NAME, + "logs-*,my_index", String.class); + + assertEquals("/logs-*%2Cmy_index/_search", single().rawPath()); + // the node decodes the segment back to the list of index names and patterns + assertEquals("/logs-*,my_index/_search", single().path()); + } + + @ParameterizedTest + @ValueSource(strings = { "my index", "a/b", "a?b", "a#b", "a\\b", "a|b", "a\"b", "<a>", ".", "..", "a,,b", "a," }) + void indexNameWithCharactersElasticsearchDoesNotAllowIsRejected(String indexName) { + Exchange exchange = template.request(uri + "&operation=GET_BY_ID", e -> { + e.getMessage().setHeader(ElasticSearchRestClientConstant.INDEX_NAME, indexName); + e.getMessage().setHeader(ElasticSearchRestClientConstant.ID, "1"); + }); + + assertInstanceOf(IllegalArgumentException.class, exchange.getException()); + assertTrue(requests.isEmpty(), "requests sent: " + requests); + } + + @Test + void getByIdResponseWithoutSourceReturnsNoDocument() { + Exchange exchange = template.request(uri + "&operation=GET_BY_ID&indexName=my_index", + e -> e.getMessage().setHeader(ElasticSearchRestClientConstant.ID, "nosource")); + + assertNull(exchange.getException()); + assertNull(exchange.getMessage().getBody()); + } + + private Recorded single() { + assertEquals(1, requests.size(), "requests sent: " + requests); + return requests.get(0); + } + + private void answer(HttpExchange exchange) throws IOException { + exchange.getRequestBody().readAllBytes(); + String method = exchange.getRequestMethod(); + String rawPath = exchange.getRequestURI().getRawPath(); + requests.add(new Recorded( + method, rawPath, exchange.getRequestURI().getPath(), + exchange.getRequestURI().getRawQuery())); + String response; + if (rawPath.endsWith("/_search")) { + response = "{\"hits\":{\"hits\":[{\"_source\":{\"title\":\"camel\"}}]}}"; + } else if (rawPath.endsWith("/_doc/nosource")) { + response = "{\"_index\":\"my_index\",\"_id\":\"nosource\",\"found\":true}"; + } else if ("DELETE".equals(method)) { + response = "{\"_index\":\"my_index\",\"_id\":\"1\",\"result\":\"deleted\"}"; + } else if ("POST".equals(method)) { + response = "{\"_index\":\"my_index\",\"_id\":\"1\",\"result\":\"created\"}"; + } else { + response = "{\"_index\":\"my_index\",\"_id\":\"1\",\"found\":true,\"_source\":{\"title\":\"camel\"}}"; + } + byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream out = exchange.getResponseBody()) { + out.write(bytes); + } + } + + private record Recorded(String method, String rawPath, String path, String rawQuery) { + } +} diff --git a/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/integration/ElasticsearchRestClientComponentIT.java b/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/integration/ElasticsearchRestClientComponentIT.java index cbb09e1af63d..422ae16a08be 100644 --- a/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/integration/ElasticsearchRestClientComponentIT.java +++ b/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/integration/ElasticsearchRestClientComponentIT.java @@ -74,6 +74,18 @@ public class ElasticsearchRestClientComponentIT extends ElasticsearchRestClientI .setHeader(ElasticSearchRestClientConstant.OPERATION).constant(ElasticsearchRestClientOperation.DELETE) .to("elasticsearch-rest-client:my-cluster?restClient=#restClient&indexName=my_index"); + from("direct:index-path-segment") + .to("elasticsearch-rest-client:my-cluster?operation=INDEX_OR_UPDATE&restClient=#restClient&indexName=path_segment_index"); + + from("direct:get-by-id-path-segment") + .to("elasticsearch-rest-client:my-cluster?operation=GET_BY_ID&restClient=#restClient&indexName=path_segment_index"); + + from("direct:search-path-segment") + .to("elasticsearch-rest-client:my-cluster?operation=SEARCH&restClient=#restClient&indexName=path_segment_*,path_segment_index"); + + from("direct:delete-path-segment") + .to("elasticsearch-rest-client:my-cluster?operation=DELETE&restClient=#restClient&indexName=path_segment_index"); + from("direct:errorHandler") .log("Handling exception: ${exception.message}") .to("mock:errorHandler"); @@ -265,6 +277,31 @@ public class ElasticsearchRestClientComponentIT extends ElasticsearchRestClientI assertTrue(ack.get()); } + @Test + void documentWhoseIdContainsReservedCharacters() throws Exception { + Awaitility.await().atMost(30, TimeUnit.SECONDS).ignoreExceptions().untilAsserted(() -> { + Response health = restClient.performRequest(new Request("GET", "/_cluster/health")); + assertEquals(200, health.getStatusLine().getStatusCode()); + }); + + var id = "a/b?c#d e%"; + var document = "{\"title\": \"Elastic with reserved characters in the id\"}"; + + assertEquals(id, template.requestBodyAndHeader("direct:index-path-segment", document, + ElasticSearchRestClientConstant.ID, id, String.class)); + + assertEquals("{\"title\":\"Elastic with reserved characters in the id\"}", + template.requestBody("direct:get-by-id-path-segment", id, String.class)); + + // the comma separated list of an index pattern and an index name still searches both + Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertEquals( + "[{\"title\":\"Elastic with reserved characters in the id\"}]", + template.requestBody("direct:search-path-segment", null, String.class))); + + assertTrue(template.requestBodyAndHeader("direct:delete-path-segment", null, ElasticSearchRestClientConstant.ID, id, + Boolean.class)); + } + @Test void operationUndefined() { Exchange exchange = template.request("elasticsearch-rest-client:my-cluster?restClient=#restClient&indexName=my_index", diff --git a/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/integration/ElasticsearchRestClientITSupport.java b/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/integration/ElasticsearchRestClientITSupport.java index 246ac6ed2414..9017a168d08a 100644 --- a/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/integration/ElasticsearchRestClientITSupport.java +++ b/components/camel-elasticsearch-rest-client/src/test/java/org/apache/camel/component/elasticsearch/rest/client/integration/ElasticsearchRestClientITSupport.java @@ -26,6 +26,7 @@ import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; @@ -44,6 +45,10 @@ public class ElasticsearchRestClientITSupport extends CamelTestSupport { @Override protected void setupResources() throws Exception { super.setupResources(); + if (restClient != null) { + // one client for all the tests of the class, as the routes of the class are bound to it + return; + } String scheme = service.getSslContext().isPresent() ? "https" : "http"; HttpHost host = new HttpHost(service.getElasticSearchHost(), service.getPort(), scheme); @@ -62,11 +67,11 @@ public class ElasticsearchRestClientITSupport extends CamelTestSupport { restClient = builder.build(); } - @Override - protected void cleanupResources() throws Exception { - super.cleanupResources(); + @AfterAll + void closeRestClient() throws Exception { if (restClient != null) { restClient.close(); + restClient = null; } } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc index 56cfa705003d..c92f0d90d86e 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc @@ -185,6 +185,18 @@ when `enableSniffer` is set, the nodes the sniffer finds, matching the scheme, h registered for any host. A request to another host, including the host of a node with another scheme or port, is now sent without them. A client set with the `restClient` option is not changed. +=== camel-elasticsearch-rest-client - the document id and the index name are each sent as one path segment + +The producer now percent-encodes the document id and the index name, so each is sent as one path segment of the +request and Elasticsearch takes it literally. Before, they were placed into the path as given: a `/`, `?` or `#` in +the value changed the path of the request, and a space or a `%` made the request fail. A route that put extra path +segments or request parameters into the document id or the index name must change. + +A document id of `.` or `..` is now rejected, as is an index name that is empty, `.` or `..`, or that contains `\`, +`/`, `?`, `"`, `<`, `>`, `|`, `#` or a space. A comma separated list of index names and patterns, such as +`logs-*,my_index`, still works. A `GET_BY_ID` response without the source of the document now gives a `null` body +instead of failing with a `NullPointerException`. + === camel-opensearch - the credentials are only sent to the nodes of the cluster The `user` and `password` are now offered only to the nodes the client talks to: the hosts of `hostAddresses` and, diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index b7790ca6a050..f15d23fdd8d8 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -246,6 +246,18 @@ when `enableSniffer` is set, the nodes the sniffer finds, matching the scheme, h registered for any host. A request to another host, including the host of a node with another scheme or port, is now sent without them. A client set with the `restClient` option is not changed. +=== camel-elasticsearch-rest-client - the document id and the index name are each sent as one path segment + +The producer now percent-encodes the document id and the index name, so each is sent as one path segment of the +request and Elasticsearch takes it literally. Before, they were placed into the path as given: a `/`, `?` or `#` in +the value changed the path of the request, and a space or a `%` made the request fail. A route that put extra path +segments or request parameters into the document id or the index name must change. + +A document id of `.` or `..` is now rejected, as is an index name that is empty, `.` or `..`, or that contains `\`, +`/`, `?`, `"`, `<`, `>`, `|`, `#` or a space. A comma separated list of index names and patterns, such as +`logs-*,my_index`, still works. A `GET_BY_ID` response without the source of the document now gives a `null` body +instead of failing with a `NullPointerException`. + === camel-smooks - allowExternalEntities applies to Smooks Source bodies and to a reader declared without a class The Smooks component and `SmooksProcessor` now parse a message body that is already a Smooks `Source` 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 03577d5cf348..4a09ff47352e 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 @@ -93,6 +93,18 @@ A route that relied on a Simple expression in a header tag or attribute should u option instead (for example `tags.customer=${header.customerId}`), or compute the value in the route before setting the header. +=== camel-elasticsearch-rest-client - the document id and the index name are each sent as one path segment + +The producer now percent-encodes the document id and the index name, so each is sent as one path segment of the +request and Elasticsearch takes it literally. Before, they were placed into the path as given: a `/`, `?` or `#` in +the value changed the path of the request, and a space or a `%` made the request fail. A route that put extra path +segments or request parameters into the document id or the index name must change. + +A document id of `.` or `..` is now rejected, as is an index name that is empty, `.` or `..`, or that contains `\`, +`/`, `?`, `"`, `<`, `>`, `|`, `#` or a space. A comma separated list of index names and patterns, such as +`logs-*,my_index`, still works. A `GET_BY_ID` response without the source of the document now gives a `null` body +instead of failing with a `NullPointerException`. + === camel-ognl removed The `camel-ognl` component has been removed. It was deprecated since Camel 4.14.
