Search code examples
springspring-bootelasticsearchspring-data-elasticsearch

Spring Data Elastic Fails on 3.1.4 - Incorrect HTTP method for uri


I've been upgrading my Spring Boot Elastic application from 2.x up to 3.x - i'm struggling with the below error that i've been able to reproduce in a fresh Spring Boot ES Application (from the Spring Boot Initialiser). the code, as you will see, is elementary.

The issue happens when persisting a document and fails with the Spring Boot 3.x (testing executed on 3.1.4 of spring boot) codebase but the with the same code tested on Spring Boot 2.7 (against ES 7.11) is successful.

Any Ideas? The gitrepo is available at https://github.com/kellizer/spring-boot-es-failure-3.1.4


The Error

Caused by: co.elastic.clients.elasticsearch._types.ElasticsearchException: [es/index] failed: [null] Incorrect HTTP method for uri [/vaultdoc/_doc/?refresh=false] and method [PUT], allowed: [POST]

The Stacktrace:

Caused by: co.elastic.clients.elasticsearch._types.ElasticsearchException: [es/index] failed: [null] Incorrect HTTP method for uri [/vaultdoc/_doc/?refresh=false] and method [PUT], allowed: [POST]
    at co.elastic.clients.transport.rest_client.RestClientTransport.getHighLevelResponse(RestClientTransport.java:334)
    at co.elastic.clients.transport.rest_client.RestClientTransport.performRequest(RestClientTransport.java:154)
    at co.elastic.clients.elasticsearch.ElasticsearchClient.index(ElasticsearchClient.java:1116)
    at org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate.lambda$doIndex$6(ElasticsearchTemplate.java:220)
    at org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate.execute(ElasticsearchTemplate.java:623)

The Test (Kotlin)

@Testcontainers
@SpringBootTest(classes = [TestApp::class], properties = ["logging.level.org.testcontainers=debug"])
@ContextConfiguration(initializers = [SetupContainers::class])
class VaultDocumentTest(@Autowired private val vaultDocumentRepo: VaultDocumentRepo) {
    @Test
    fun simpleTest() {
        println("Count Call Works-->${vaultDocumentRepo.count()}")//this works - returns 0
        println("findAll Works -->${vaultDocumentRepo.findAll().toList().size}") //this works - returns 0
        vaultDocumentRepo.save(VaultDocument("Test101")) //save fails with error '[es/index] failed: [null] Incorrect HTTP method for uri [/vaultdoc/_doc/?refresh=false] and method [PUT], allowed: [POST]'
    }
}

The Document

@Document(indexName = "vaultdoc")
class VaultDocument(
    @Field(type = FieldType.Text, store = true, fielddata = true)
    val vaultDocumentName: String,
    @org.springframework.data.annotation.Id
    var id: String = "",
)

The Repository

@Repository
interface VaultDocumentRepo : ElasticsearchRepository<VaultDocument, String> {
    fun findByVaultDocumentName(vaultDocumentName: String): VaultDocument?
}

And finally, how the testcontainer is setup.

class SetupContainers : ApplicationContextInitializer<ConfigurableApplicationContext> {
override fun initialize(applicationContext: ConfigurableApplicationContext) {

    elasticsearchContainer.withEnv(
        mapOf(
            "xpack.security.enabled" to "false",
            "xpack.security.http.ssl.enabled" to "false",
            "action.destructive_requires_name" to "false",
            "reindex.remote.whitelist" to "localhost:9200"
        )
    )
    elasticsearchContainer.start()
    TestPropertyValues.of(
        "spring.elasticsearch.uris=http://${elasticsearchContainer.httpHostAddress}",
    ).applyTo(applicationContext.environment)
}


companion object {
    @Container
    val elasticsearchContainer: ElasticsearchContainer = ElasticsearchContainer(
        DockerImageName
            .parse("docker.elastic.co/elasticsearch/elasticsearch")
            .withTag("8.10.0")

    )
}

}


Solution

  • (The download error came from an overly harsh configured pihole that blocked "downloads.gradle.org".)

    As for the PUT and POST: I had a look at the Elasticsearch client code for IndexRequest. It will send a PUT when indexing a document where the id is not null and a POST otherwise:

                // Request method
                request -> {
                    final int _index = 1 << 0;
                    final int _id = 1 << 1;
    
                    int propsSet = 0;
    
                    propsSet |= _index;
                    if (request.id() != null)
                        propsSet |= _id;
    
                    if (propsSet == (_index | _id))
                        return "PUT";
                    if (propsSet == (_index))
                        return "POST";
                    throw SimpleEndpoint.noPathTemplateFound("method");
    
                },
    

    In your code you set the id to a default value of an empty string, which of course is not null

    @Document(indexName = "vaultdoc")
    class VaultDocument(
        @Field(type = FieldType.Text, store = true, fielddata = true)
        val vaultDocumentName: String,
        @org.springframework.data.annotation.Id
        var id: String = "",
    )
    

    and in your test you don't specify an id, so we have an empty string there:

    vaultDocumentRepo.save(VaultDocument("Test101"))
    

    Therefore a PUT is sent, but the empty id is put in the url /vaultdoc/_doc/<empty id here>?refresh=false, but that's the request for a new document without an id which should be POSTed.

    You can either make your id nullable and set the default to null:

    var id: String? = null
    

    or you remove the default empty string and provide a value for every document you save.

    Why didn't it fail with Spring Data Elasticsearch 2? I don't dig out Elasticsearch's old code, but I suppose that the old RestHighLevelClient did not only check for null but also for an empty string as well