From 38d3e490f03a84a99f46e95dbaf980b96d6a469f Mon Sep 17 00:00:00 2001 From: Fernando Briano Date: Thu, 28 May 2026 14:14:02 +0100 Subject: [PATCH 1/2] Avoid duplicated content-type headers Adds tests for Manticore, depends on changes in transport: https://github.com/elastic/elastic-transport-ruby/pull/115 Addresses #2961 --- elasticsearch/Gemfile | 3 + elasticsearch/lib/elasticsearch.rb | 4 +- elasticsearch/spec/unit/headers_spec.rb | 89 +++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 9 deletions(-) diff --git a/elasticsearch/Gemfile b/elasticsearch/Gemfile index 21c38fe230..9fd65ae5be 100644 --- a/elasticsearch/Gemfile +++ b/elasticsearch/Gemfile @@ -31,3 +31,6 @@ if ENV['TRANSPORT_VERSION'] == 'main' end gem 'opentelemetry-sdk', require: false if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0') +if defined?(JRUBY_VERSION) + gem 'manticore', platform: :jruby +end diff --git a/elasticsearch/lib/elasticsearch.rb b/elasticsearch/lib/elasticsearch.rb index 3bf2e79302..e27c79cd45 100644 --- a/elasticsearch/lib/elasticsearch.rb +++ b/elasticsearch/lib/elasticsearch.rb @@ -189,10 +189,10 @@ def set_user_agent!(arguments) def set_content_type!(arguments) headers = {} user_headers = arguments&.[](:transport_options)&.[](:headers) - unless user_headers&.keys&.detect { |h| h =~ /content-?_?type/ } + unless user_headers&.keys&.detect { |h| h =~ /content-?_?type/i } headers['content-type'] = 'application/vnd.elasticsearch+json; compatible-with=9' end - unless user_headers&.keys&.detect { |h| h =~ /accept/ } + unless user_headers&.keys&.detect { |h| h =~ /accept/i } headers['accept'] = 'application/vnd.elasticsearch+json; compatible-with=9' end set_header(headers, arguments) unless headers.empty? diff --git a/elasticsearch/spec/unit/headers_spec.rb b/elasticsearch/spec/unit/headers_spec.rb index 3751c74488..f8d516fc26 100644 --- a/elasticsearch/spec/unit/headers_spec.rb +++ b/elasticsearch/spec/unit/headers_spec.rb @@ -73,20 +73,18 @@ expect_any_instance_of(Faraday::Connection) .to receive(:run_request) - .with(:get, 'http://localhost:9200/_search', nil, connection_headers) { OpenStruct.new(body: '') } + .with(:get, 'http://localhost:9200/_search', nil, connection_headers) { OpenStruct.new(body: '') } client.search end end context 'when content-type header is changed' do let!(:client) do - described_class.new( - host: 'http://localhost:9200', - transport_options: { headers: instance_headers } - ).tap do |client| + described_class.new(transport_options: { headers: instance_headers }).tap do |client| client.instance_variable_set('@verified', true) end end + let(:instance_headers) do { content_type: 'application/json' } end @@ -98,11 +96,88 @@ expect_any_instance_of(Faraday::Connection) .to receive(:run_request) - .with(:get, 'http://localhost:9200/_search', nil, connection_headers) { OpenStruct.new(body: '') } + .with(:get, 'http://localhost:9200/_search', nil, connection_headers) { OpenStruct.new(body: '') } + client.search + end + end + + context 'when Content-Type header is used' do + let(:client) do + described_class.new(transport_options: { headers: instance_headers }).tap do |client| + client.instance_variable_set('@verified', true) + end + end + + let(:instance_headers) do + { 'Content-Type' => 'application/text' } + end + + it 'performs the request with the header' do + connection_headers = client.transport.connections.connections.first.connection.headers + expect(connection_headers['Accept']).to eq 'application/vnd.elasticsearch+json; compatible-with=9' + expect(connection_headers['Content-Type']).to eq 'application/text' + + expect_any_instance_of(Faraday::Connection) + .to receive(:run_request) + .with(:get, 'http://localhost:9200/_search', nil, connection_headers) { OpenStruct.new(body: '') } client.search end end + if defined?(JRUBY_VERSION) + context 'Using Manticore with JRuby' do + let(:client) do + require 'elastic/transport/transport/http/manticore' + described_class.new( + transport_class: Elastic::Transport::Transport::HTTP::Manticore, + transport_options: { headers: instance_headers } + ).tap do |client| + client.instance_variable_set('@verified', true) + end + end + + context 'when Content-Type header is used' do + let(:instance_headers) do + { 'Content-Type' => 'application/text' } + end + + it 'does not duplicate content-type the header' do + connection_headers = client.transport.connections.connections.first.connection.instance_variable_get('@options')[:headers] + expect(connection_headers['accept']).to eq 'application/vnd.elasticsearch+json; compatible-with=9' + expect(connection_headers['content-type']).to be nil + expect(connection_headers.fetch('Content-Type')).to eq 'application/text' + + expect_any_instance_of(Manticore::Client) + .to receive(:get).with( + 'http://localhost:9200/_search', + { headers: connection_headers } + ) { OpenStruct.new(body: '') } + client.search + end + + context 'when content-type header is used' do + let(:instance_headers) do + { 'content-type' => 'application/text' } + end + + it 'does not duplicate Content-Type the header' do + connection_headers = client.transport.connections.connections.first.connection.instance_variable_get('@options')[:headers] + expect(connection_headers['accept']).to eq 'application/vnd.elasticsearch+json; compatible-with=9' + expect(connection_headers['content-type']).to be nil + expect(connection_headers.fetch('Content-Type')).to eq 'application/text' + + expect_any_instance_of(Manticore::Client) + .to receive(:get).with( + 'http://localhost:9200/_search', + { headers: connection_headers } + ) { OpenStruct.new(body: '') } + client.search + end + end + end + end + end + context 'when no header is set, uses v9 content-type and accept' do let!(:client) do described_class.new(host: 'http://localhost:9200').tap do |client| @@ -117,7 +192,7 @@ expect_any_instance_of(Faraday::Connection) .to receive(:run_request) - .with(:get, 'http://localhost:9200/_search', nil, expected_headers) { OpenStruct.new(body: '') } + .with(:get, 'http://localhost:9200/_search', nil, expected_headers) { OpenStruct.new(body: '') } client.search end end From 6fc4cf4d4d0276bd699a439d477c07476a562285 Mon Sep 17 00:00:00 2001 From: Fernando Briano Date: Wed, 3 Jun 2026 10:36:31 +0100 Subject: [PATCH 2/2] [Tests] Separates header tests for JRuby --- elasticsearch/spec/unit/headers_jruby_spec.rb | 111 ++++++++++++++++++ elasticsearch/spec/unit/headers_spec.rb | 54 --------- 2 files changed, 111 insertions(+), 54 deletions(-) create mode 100644 elasticsearch/spec/unit/headers_jruby_spec.rb diff --git a/elasticsearch/spec/unit/headers_jruby_spec.rb b/elasticsearch/spec/unit/headers_jruby_spec.rb new file mode 100644 index 0000000000..0255acf02f --- /dev/null +++ b/elasticsearch/spec/unit/headers_jruby_spec.rb @@ -0,0 +1,111 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. 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. +return unless defined?(JRUBY_VERSION) + +require 'spec_helper' +require 'ostruct' + +describe Elasticsearch::Client do + context 'Using Manticore with JRuby' do + let(:client) do + require 'elastic/transport/transport/http/manticore' + described_class.new( + transport_class: Elastic::Transport::Transport::HTTP::Manticore, + transport_options: { headers: instance_headers } + ).tap do |client| + client.instance_variable_set('@verified', true) + end + end + + let(:client_headers) do + client.transport.connections.connections.first.connection.instance_variable_get('@options')[:headers] + end + + context 'when Content-Type header is used' do + let(:instance_headers) do + { 'Content-Type' => 'application/text' } + end + + it 'does not duplicate the content-type header' do + expect(client_headers['accept']).to eq 'application/vnd.elasticsearch+json; compatible-with=9' + expect(client_headers['content-type']).to be nil + expect(client_headers.fetch('Content-Type')).to eq 'application/text' + + expect_any_instance_of(Manticore::Client) + .to receive(:get).with( + 'http://localhost:9200/_search', + { headers: client_headers } + ) { OpenStruct.new(body: '') } + client.search + end + + context 'when content-type header is used' do + let(:instance_headers) do + { 'content-type' => 'application/text' } + end + + it 'does not duplicate the Content-Type header' do + expect(client_headers['accept']).to eq 'application/vnd.elasticsearch+json; compatible-with=9' + expect(client_headers['content-type']).to be nil + expect(client_headers.fetch('Content-Type')).to eq 'application/text' + + expect_any_instance_of(Manticore::Client) + .to receive(:get).with( + 'http://localhost:9200/_search', + { headers: client_headers } + ) { OpenStruct.new(body: '') } + client.search + end + end + + context 'when Content-Type is set in request' do + let(:instance_headers) do + { 'Content-Type' => 'instance_headers' } + end + let(:request_headers) { { 'Content-Type' => 'request headers' } } + + it 'does not duplicate content-type headers' do + expect_any_instance_of(Manticore::Client) + .to receive(:get).with( + 'http://localhost:9200/_search', + { headers: client_headers.merge(request_headers) } + ) { OpenStruct.new(body: '') } + client.search(headers: request_headers) + puts client_headers.merge(request_headers) + end + end + + context 'when content-type is set in request' do + let(:instance_headers) do + { 'Content-Type' => 'instance_headers' } + end + let(:request_headers) { { 'content-type' => 'request headers' } } + + it 'does not duplicate content-type headers' do + expect_any_instance_of(Manticore::Client) + .to receive(:get).with( + 'http://localhost:9200/_search', + { headers: client_headers.merge(request_headers) } + ) { OpenStruct.new(body: '') } + client.search(headers: request_headers) + puts client_headers.merge(request_headers) + + end + end + end + end +end diff --git a/elasticsearch/spec/unit/headers_spec.rb b/elasticsearch/spec/unit/headers_spec.rb index f8d516fc26..a6155032d8 100644 --- a/elasticsearch/spec/unit/headers_spec.rb +++ b/elasticsearch/spec/unit/headers_spec.rb @@ -124,60 +124,6 @@ end end - if defined?(JRUBY_VERSION) - context 'Using Manticore with JRuby' do - let(:client) do - require 'elastic/transport/transport/http/manticore' - described_class.new( - transport_class: Elastic::Transport::Transport::HTTP::Manticore, - transport_options: { headers: instance_headers } - ).tap do |client| - client.instance_variable_set('@verified', true) - end - end - - context 'when Content-Type header is used' do - let(:instance_headers) do - { 'Content-Type' => 'application/text' } - end - - it 'does not duplicate content-type the header' do - connection_headers = client.transport.connections.connections.first.connection.instance_variable_get('@options')[:headers] - expect(connection_headers['accept']).to eq 'application/vnd.elasticsearch+json; compatible-with=9' - expect(connection_headers['content-type']).to be nil - expect(connection_headers.fetch('Content-Type')).to eq 'application/text' - - expect_any_instance_of(Manticore::Client) - .to receive(:get).with( - 'http://localhost:9200/_search', - { headers: connection_headers } - ) { OpenStruct.new(body: '') } - client.search - end - - context 'when content-type header is used' do - let(:instance_headers) do - { 'content-type' => 'application/text' } - end - - it 'does not duplicate Content-Type the header' do - connection_headers = client.transport.connections.connections.first.connection.instance_variable_get('@options')[:headers] - expect(connection_headers['accept']).to eq 'application/vnd.elasticsearch+json; compatible-with=9' - expect(connection_headers['content-type']).to be nil - expect(connection_headers.fetch('Content-Type')).to eq 'application/text' - - expect_any_instance_of(Manticore::Client) - .to receive(:get).with( - 'http://localhost:9200/_search', - { headers: connection_headers } - ) { OpenStruct.new(body: '') } - client.search - end - end - end - end - end - context 'when no header is set, uses v9 content-type and accept' do let!(:client) do described_class.new(host: 'http://localhost:9200').tap do |client|