elastic/elasticsearch-java

Bulk indexing fails with INDENT_OUTPUT configured on JacksonJsonpMapper's ObjectMapper

dconnelly opened this issue · 0 comments

Java API client version

8.9.0

Java version

17

Elasticsearch Version

8.9.0

Problem description

Was trying to enable SerializationFeature.INDENT_OUTPUT with JacksonJsonpMapper's ObjectMapper in order to make traces easier to read during local testing and debugging. Unfortunately, this seems to cause the ES client to generate a malformed request:

20:31:52.690 [main] TRACE tracer - curl -iX POST 'https://localhost:52608/_bulk' -d '{"index":{"_index":"test"}}
{
  "name" : "book",
  "description" : "a small book"
}
'
# HTTP/1.1 400 Bad Request
# X-elastic-product: Elasticsearch
# content-type: application/vnd.elasticsearch+json;compatible-with=8
# content-length: 301
#
# {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"Malformed action/metadata line [3], expected START_OBJECT but found [VALUE_STRING]"}],"type":"illegal_argument_exception","reason":"Malformed action/metadata line [3], expected START_OBJECT but found [VALUE_STRING]"},"status":400}

Reproducer:

package com.yammer.embeddings.elastic;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.BulkRequest;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.utility.DockerImageName;

class BulkBugTest {
  static final ElasticsearchContainer container = new ElasticsearchContainer(
      DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch")
          .withTag("8.9.0"));

  static final String INDEX_NAME = "test";

  static RestClient restClient;

  record Item(String name, String description) {}

  @BeforeAll
  static void init() throws Exception {
    container.start();
    var credentials = new BasicCredentialsProvider();
    credentials.setCredentials(
        AuthScope.ANY, new UsernamePasswordCredentials(
            "elastic", ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
    );
    restClient = RestClient.builder(HttpHost.create("https://" + container.getHttpHostAddress()))
        .setHttpClientConfigCallback(builder -> builder
            .setSSLContext(container.createSslContextFromCa())
            .setDefaultCredentialsProvider(credentials))
        .build();
    var client = new ElasticsearchClient(
        new RestClientTransport(restClient, new JacksonJsonpMapper()));
    client.indices().create(req -> req
        .index(INDEX_NAME)
        .mappings(mappings -> mappings
            .properties("name", b -> b.keyword(k -> k.index(true)))
            .properties("description", b -> b.keyword(k -> k.index(false)))
        )
    );
  }

  @AfterAll
  static void destroy() throws Exception {
    if (restClient != null) {
      restClient.close();
    }
    container.stop();
  }

  @Test
  void succeedsWithoutIndent() throws Exception {
    var client = new ElasticsearchClient(
        new RestClientTransport(restClient, new JacksonJsonpMapper()));
    var item = new Item("book", "a small book");
    var bulk = new BulkRequest.Builder();
    bulk.operations(op -> op.index(index -> index.index(INDEX_NAME).document(item)));
    var response = client.bulk(bulk.build());
  }

  @Test
  void failsWithIndent() throws Exception {
    var mapper = new ObjectMapper()
        .configure(SerializationFeature.INDENT_OUTPUT, true);
    var client = new ElasticsearchClient(
        new RestClientTransport(restClient, new JacksonJsonpMapper(mapper)));
    var item = new Item("book", "a small book");
    var bulk = new BulkRequest.Builder();
    bulk.operations(op -> op.index(index -> index.index(INDEX_NAME).document(item)));
    var response = client.bulk(bulk.build());
  }
}