Skip to content

Commit 1e739e9

Browse files
authored
Merge pull request #112 from tungleduyxyz/fix_url_encoding_error
Fix issue with URL encoding
2 parents 30e7c3e + 9b045e4 commit 1e739e9

File tree

2 files changed

+157
-3
lines changed

2 files changed

+157
-3
lines changed

lib/killbill_client/api/net_http_adapter.rb

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,17 @@ def net_http
3636
}
3737

3838
def build_uri(relative_uri, options)
39-
# Need to encode in case of spaces (e.g. /1.0/kb/security/users/Mad Max/roles)
40-
encoded_relative_uri = URI::DEFAULT_PARSER.regexp[:UNSAFE].match?(relative_uri) ? relative_uri : URI::DEFAULT_PARSER.escape(relative_uri)
39+
# Split the URI into path and query parts
40+
uri_parts = relative_uri.split('?', 2)
41+
path_part = uri_parts[0]
42+
query_part = uri_parts[1]
43+
unsafe_regex = /[^a-zA-Z0-9\-_.!~*'():\/]/
44+
# Only encode the path part, not the query string
45+
encoded_path = unsafe_regex.match?(path_part) ? CGI.escape(path_part) : path_part
46+
47+
# Reconstruct the relative_uri with encoded path
48+
encoded_relative_uri = query_part ? "#{encoded_path}?#{query_part}" : encoded_path
49+
4150
if URI(encoded_relative_uri).scheme.nil?
4251
uri = (options[:base_uri] || KillBillClient::API.base_uri)
4352
uri = URI.parse(uri) unless uri.is_a?(URI)
@@ -49,7 +58,19 @@ def build_uri(relative_uri, options)
4958
uri = encoded_relative_uri
5059
uri = URI.parse(uri) unless uri.is_a?(URI)
5160
end
52-
uri += encode_params(options).to_s
61+
62+
query_params = encode_params(options)
63+
if query_params && !query_params.empty?
64+
# encode_params returns "?param=value", so remove the leading "?"
65+
params_without_question = query_params[1..-1]
66+
if uri.query && !uri.query.empty?
67+
# If there's already a query string, append with &
68+
uri.query = uri.query + '&' + params_without_question
69+
else
70+
# If no existing query string, set it directly
71+
uri.query = params_without_question
72+
end
73+
end
5374

5475
uri
5576
end

spec/killbill_client/http_adapter_spec.rb

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,139 @@
106106
uri = http_adapter.send(:build_uri, KillBillClient::Model::Account::KILLBILL_API_ACCOUNTS_PREFIX, options)
107107
expect(uri).to eq(URI.parse("#{KillBillClient::API.base_uri.to_s}/1.0/kb/accounts"))
108108
end
109+
110+
describe '#build_uri' do
111+
let(:http_adapter) { DummyForHTTPAdapter.new }
112+
113+
before do
114+
KillBillClient.url = 'http://example.com:8080'
115+
end
116+
117+
context 'with basic relative URI' do
118+
it 'should combine base URI with relative URI' do
119+
relative_uri = '/1.0/kb/accounts'
120+
options = {}
121+
uri = http_adapter.send(:build_uri, relative_uri, options)
122+
expect(uri.to_s).to eq('http://example.com:8080/1.0/kb/accounts')
123+
end
124+
125+
it 'should handle relative URI without leading slash' do
126+
relative_uri = '1.0/kb/accounts'
127+
options = {}
128+
uri = http_adapter.send(:build_uri, relative_uri, options)
129+
expect(uri.to_s).to eq('http://example.com:8080/1.0/kb/accounts')
130+
end
131+
end
132+
133+
context 'with custom base_uri in options' do
134+
it 'should use custom base_uri from options' do
135+
relative_uri = '/1.0/kb/accounts'
136+
options = { base_uri: 'https://custom.example.com:9090' }
137+
uri = http_adapter.send(:build_uri, relative_uri, options)
138+
expect(uri.to_s).to eq('https://custom.example.com:9090/1.0/kb/accounts')
139+
end
140+
end
141+
142+
context 'with absolute URI' do
143+
it 'should use absolute URI as-is' do
144+
relative_uri = 'https://absolute.example.com/api/test'
145+
options = {}
146+
uri = http_adapter.send(:build_uri, relative_uri, options)
147+
expect(uri.to_s).to eq('https://absolute.example.com/api/test')
148+
end
149+
end
150+
151+
context 'with path encoding' do
152+
it 'should handle properly encoded URIs (with double encoding)' do
153+
relative_uri = '/1.0/kb/accounts/my%20account%20with%20spaces'
154+
options = {}
155+
uri = http_adapter.send(:build_uri, relative_uri, options)
156+
expect(uri.to_s).to eq('http://example.com:8080/%2F1.0%2Fkb%2Faccounts%2Fmy%2520account%2520with%2520spaces')
157+
end
158+
159+
it 'should not encode safe characters in path' do
160+
relative_uri = '/1.0/kb/accounts/abc-1234'
161+
options = {}
162+
uri = http_adapter.send(:build_uri, relative_uri, options)
163+
expect(uri.to_s).to eq('http://example.com:8080/1.0/kb/accounts/abc-1234')
164+
end
165+
166+
it 'should handle properly encoded URI with query string (with double encoding)' do
167+
relative_uri = '/1.0/kb/accounts/my%20account?search=test%20value'
168+
options = {}
169+
uri = http_adapter.send(:build_uri, relative_uri, options)
170+
expect(uri.to_s).to eq('http://example.com:8080/%2F1.0%2Fkb%2Faccounts%2Fmy%2520account?search=test%20value')
171+
end
172+
end
173+
174+
context 'with query parameters in options' do
175+
it 'should add simple query parameters' do
176+
relative_uri = '/1.0/kb/accounts'
177+
options = { params: { accountId: '123', limit: 10 } }
178+
uri = http_adapter.send(:build_uri, relative_uri, options)
179+
expect(uri.query).to include('accountId=123')
180+
expect(uri.query).to include('limit=10')
181+
end
182+
183+
it 'should handle array parameters' do
184+
relative_uri = '/1.0/kb/accounts'
185+
options = { params: { tags: ['premium', 'business'] } }
186+
uri = http_adapter.send(:build_uri, relative_uri, options)
187+
expect(uri.query).to include('tags=premium')
188+
expect(uri.query).to include('tags=business')
189+
end
190+
191+
it 'should merge with existing query string in relative URI' do
192+
relative_uri = '/1.0/kb/accounts?existing=value'
193+
options = { params: { new: 'param' } }
194+
uri = http_adapter.send(:build_uri, relative_uri, options)
195+
expect(uri.query).to include('existing=value')
196+
expect(uri.query).to include('new=param')
197+
end
198+
199+
it 'should handle empty params hash' do
200+
relative_uri = '/1.0/kb/accounts'
201+
options = { params: {} }
202+
uri = http_adapter.send(:build_uri, relative_uri, options)
203+
expect(uri.query).to be_nil
204+
end
205+
206+
it 'should encode special characters in query parameters' do
207+
relative_uri = '/1.0/kb/accounts'
208+
options = { params: { search: 'test & special chars' } }
209+
uri = http_adapter.send(:build_uri, relative_uri, options)
210+
expect(uri.query).to include('search=test+%26+special+chars')
211+
end
212+
end
213+
214+
context 'with plugin properties' do
215+
it 'should handle pluginProperty option' do
216+
contract_property = KillBillClient::Model::PluginPropertyAttributes.new
217+
contract_property.key = :contractId
218+
contract_property.value = 'test-123'
219+
relative_uri = '/1.0/kb/accounts'
220+
options = {
221+
params: {},
222+
pluginProperty: [contract_property]
223+
}
224+
uri = http_adapter.send(:build_uri, relative_uri, options)
225+
expect(uri.query).to include('pluginProperty=contractId%3Dtest-123')
226+
end
227+
end
228+
229+
context 'with control plugin names' do
230+
it 'should handle controlPluginNames option' do
231+
relative_uri = '/1.0/kb/accounts'
232+
options = {
233+
params: {},
234+
controlPluginNames: ['plugin1', 'plugin2']
235+
}
236+
uri = http_adapter.send(:build_uri, relative_uri, options)
237+
expect(uri.query).to include('controlPluginName=plugin1')
238+
expect(uri.query).to include('controlPluginName=plugin2')
239+
end
240+
end
241+
end
109242
end
110243

111244
class DummyForHTTPAdapter

0 commit comments

Comments
 (0)