Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions lib/typesense/api_call.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,14 @@ def perform_request(method, endpoint, query_parameters: nil, body_parameters: ni
@logger.debug "Attempting #{method.to_s.upcase} request Try ##{num_tries} to Node #{node[:index]}"

begin
conn, request_path = connection_and_path_for(endpoint, node)
conn, request_url = connection_and_url_for(endpoint, node)

headers = default_headers.merge(additional_headers)

response = conn.send(method, request_path) do |req|
# The URL is set on the request instead of being passed to the verb. Faraday
# looks up proxy settings again for every absolute URL passed to the verb.
response = conn.send(method) do |req|
req.url(request_url) unless request_url.nil?
req.headers = headers
req.params = query_parameters unless query_parameters.nil?
unless body_parameters.nil?
Expand Down Expand Up @@ -130,17 +133,21 @@ def perform_request(method, endpoint, query_parameters: nil, body_parameters: ni

private

# Faraday 2.9.2 and later only treat a request URL as absolute when it starts
# with a lower-case "http://" or "https://".
def uri_for(endpoint, node)
"#{node[:protocol]}://#{node[:host]}:#{node[:port]}#{endpoint}"
"#{node[:protocol].to_s.downcase}://#{node[:host]}:#{node[:port]}#{endpoint}"
end

# Returns [connection, request_path]. When keep-alive is enabled, the connection
# is cached per (thread, node) and the path is appended at request time. When it
# Returns [connection, request_url]. When keep-alive is enabled, the connection
# is cached per (thread, node) and each request gets the full URL. Faraday merges
# a relative path into the base URL, and that merge drops "." and ".." segments.
# A delete of document ".." would then delete its whole collection. When keep-alive
# is disabled, the original behaviour is preserved: a fresh Faraday is built for
# the full per-request URL, so existing callers and stubs see no change.
def connection_and_path_for(endpoint, node)
def connection_and_url_for(endpoint, node)
if @keep_alive_connections
[connection_for(node), endpoint]
[connection_for(node), uri_for(endpoint, node)]
else
[build_one_shot_connection(endpoint, node), nil]
end
Expand Down
82 changes: 82 additions & 0 deletions spec/typesense/api_call_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,88 @@
end
end

describe 'request paths with stubbed nodes' do
let(:endpoints) do
[
'/collections/c/documents/..',
'/collections/c/documents/.',
'/collections/../documents/x',
'/collections/c/documents/a+b',
'/collections/c/documents/a%20b',
'/collections/c/documents/a%2Fb'
]
end
let(:expected_targets) { endpoints.map { |endpoint| "#{endpoint}?q=a+b" } }
let(:request_targets) { [] }

before do
stub_request(:any, /node0:8108/)
.to_return(status: 200, body: JSON.dump('ok' => true), headers: { 'Content-Type' => 'application/json' })

# WebMock normalizes the URI it records and drops dot segments. Read the
# request target that Faraday hands to Net::HTTP instead (the 4th argument).
allow(Net::HTTPGenericRequest).to receive(:new).and_wrap_original do |original, *args|
request_targets << args[3]
original.call(*args)
end
end

def request_targets_for(keep_alive:, protocol: 'http')
client = Typesense::Client.new(
api_key: 'abcd',
nodes: [{ host: 'node0', port: 8108, protocol: protocol }],
log_level: Logger::ERROR,
keep_alive_connections: keep_alive
)
api_call = described_class.new(client.configuration)
endpoints.each { |endpoint| api_call.delete(endpoint, 'q' => 'a b') }
request_targets
end

it 'sends the endpoint and query unchanged with one-shot connections' do
expect(request_targets_for(keep_alive: false)).to eq(expected_targets)
end

it 'sends the endpoint and query unchanged with keep-alive connections' do
expect(request_targets_for(keep_alive: true)).to eq(expected_targets)
end

it 'sends the endpoint and query unchanged with keep-alive connections and an upper-case protocol' do
expect(request_targets_for(keep_alive: true, protocol: 'HTTP')).to eq(expected_targets)
end
end

describe 'keep-alive requests on a real Typesense server' do
let(:client) do
Typesense::Client.new(
api_key: 'xyz',
nodes: [{ host: 'localhost', port: 8108, protocol: 'http' }],
log_level: Logger::ERROR,
keep_alive_connections: true
)
end
let(:collection) { client.collections['api_call_dot_segments'] }

def delete_collection
collection.delete
rescue Typesense::Error::ObjectNotFound
# Already gone
end

before do
delete_collection
client.collections.create('name' => 'api_call_dot_segments', 'fields' => [{ 'name' => 'title', 'type' => 'string' }])
%w[x ..].each { |id| collection.documents.create('id' => id, 'title' => id) }
end

after { delete_collection }

it 'deletes only the document whose id is ".."' do
expect(collection.documents['..'].delete['id']).to eq('..')
expect(collection.documents['x'].retrieve['id']).to eq('x')
end
end

describe 'keep-alive disabled (default)' do
it 'is off by default on the configuration' do
expect(typesense.configuration.keep_alive_connections).to be(false)
Expand Down
Loading