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")
)
}
}
(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 POST
ed.
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