Skip to content

Commit 37d0099

Browse files
authored
fix: strip trailing slashes in BaseService.set_service_url (#130)
This commit modifies the BaseService.set_service_url() function so that it removes any trailing slashes. Previously, a service URL containing a trailing slash (e.g. "https://myserver.com/api/v1/") would result in a request URL that contains two slashes between the service URL and the operation path (e.g. "https://myserver.com/api/v1//myoperationpath"), and some server implementations might fail to process such a request properly. With the changes in this commit, a request URL of "https://myserver.com/api/v1/myoperationpath" will be used instead.
1 parent 1e29bfa commit 37d0099

File tree

3 files changed

+65
-27
lines changed

3 files changed

+65
-27
lines changed

ibm_cloud_sdk_core/base_service.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ def set_service_url(self, service_url: str) -> None:
224224
'The service url shouldn\'t start or end with curly brackets or quotes. '
225225
'Be sure to remove any {} and \" characters surrounding your service url'
226226
)
227+
if service_url is not None:
228+
service_url = service_url.rstrip('/')
227229
self.service_url = service_url
228230

229231
def get_http_client(self) -> requests.sessions.Session:
@@ -368,6 +370,10 @@ def prepare_request(self,
368370
# validate the service url is set
369371
if not self.service_url:
370372
raise ValueError('The service_url is required')
373+
374+
# Combine the service_url and operation path to form the request url.
375+
# Note: we have already stripped any trailing slashes from the service_url
376+
# and we know that the operation path ('url') will start with a slash.
371377
request['url'] = strip_extra_slashes(self.service_url + url)
372378

373379
headers = remove_null_values(headers) if headers else {}

test/test_base_service.py

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from ibm_cloud_sdk_core import get_authenticator_from_environment
2121
from ibm_cloud_sdk_core.authenticators import (IAMAuthenticator, NoAuthAuthenticator, Authenticator,
2222
BasicAuthenticator, CloudPakForDataAuthenticator)
23-
from ibm_cloud_sdk_core.utils import strip_extra_slashes
2423

2524

2625
class IncludeExternalConfigService(BaseService):
@@ -698,6 +697,7 @@ def test_user_agent_header():
698697
assert response.get_result().request.headers.__getitem__(
699698
'user-agent') == user_agent_header['User-Agent']
700699

700+
701701
@responses.activate
702702
def test_reserved_keys(caplog):
703703
service = AnyServiceV1('2021-07-02', authenticator=NoAuthAuthenticator())
@@ -723,6 +723,7 @@ def test_reserved_keys(caplog):
723723
assert caplog.record_tuples[2][2] == '"headers" has been removed from the request'
724724
assert caplog.record_tuples[3][2] == '"cookies" has been removed from the request'
725725

726+
726727
@responses.activate
727728
def test_ssl_error():
728729
responses.add(
@@ -810,56 +811,71 @@ def test_json():
810811
service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator())
811812
req = service.prepare_request('POST', url='', headers={
812813
'X-opt-out': True}, data={'hello': 'world', 'fóó': 'bår'})
813-
assert req.get('data') == b'{"hello": "world", "f\\u00f3\\u00f3": "b\\u00e5r"}'
814+
assert req.get(
815+
'data') == b'{"hello": "world", "f\\u00f3\\u00f3": "b\\u00e5r"}'
814816

815817

816-
def test_trailing_slash():
817-
assert strip_extra_slashes('') == ''
818-
assert strip_extra_slashes('//') == '/'
819-
assert strip_extra_slashes('/////') == '/'
820-
assert strip_extra_slashes('https://host') == 'https://host'
821-
assert strip_extra_slashes('https://host/') == 'https://host/'
822-
assert strip_extra_slashes('https://host//') == 'https://host/'
823-
assert strip_extra_slashes('https://host/path') == 'https://host/path'
824-
assert strip_extra_slashes('https://host/path/') == 'https://host/path/'
825-
assert strip_extra_slashes('https://host/path//') == 'https://host/path/'
826-
assert strip_extra_slashes('https://host//path//') == 'https://host//path/'
827-
818+
def test_service_url_handling():
828819
service = AnyServiceV1(
829-
'2018-11-20', service_url='https://host/', authenticator=NoAuthAuthenticator())
830-
assert service.service_url == 'https://host/'
820+
'2018-11-20', service_url='https://host///////', authenticator=NoAuthAuthenticator())
821+
assert service.service_url == 'https://host'
822+
831823
service.set_service_url('https://host/')
832-
assert service.service_url == 'https://host/'
824+
assert service.service_url == 'https://host'
825+
833826
req = service.prepare_request('POST',
834827
url='/path/',
835828
headers={'X-opt-out': True},
836829
data={'hello': 'world'})
837-
assert req.get('url') == 'https://host//path/'
830+
assert req.get('url') == 'https://host/path/'
838831

839832
service = AnyServiceV1(
840833
'2018-11-20', service_url='https://host/', authenticator=NoAuthAuthenticator())
841-
assert service.service_url == 'https://host/'
834+
assert service.service_url == 'https://host'
835+
842836
service.set_service_url('https://host/')
843-
assert service.service_url == 'https://host/'
837+
assert service.service_url == 'https://host'
838+
844839
req = service.prepare_request('POST',
845840
url='/',
846841
headers={'X-opt-out': True},
847842
data={'hello': 'world'})
848843
assert req.get('url') == 'https://host/'
849844

845+
req = service.prepare_request('POST',
846+
url='////',
847+
headers={'X-opt-out': True},
848+
data={'hello': 'world'})
849+
assert req.get('url') == 'https://host/'
850+
850851
service.set_service_url(None)
851852
assert service.service_url is None
852853

853854
service = AnyServiceV1('2018-11-20', service_url='/',
854855
authenticator=NoAuthAuthenticator())
855-
assert service.service_url == '/'
856+
assert service.service_url == ''
857+
856858
service.set_service_url('/')
857-
assert service.service_url == '/'
858-
req = service.prepare_request('POST',
859-
url='/',
860-
headers={'X-opt-out': True},
861-
data={'hello': 'world'})
862-
assert req.get('url') == '/'
859+
assert service.service_url == ''
860+
861+
with pytest.raises(ValueError) as err:
862+
service.prepare_request('POST',
863+
url='/',
864+
headers={'X-opt-out': True},
865+
data={'hello': 'world'})
866+
assert str(err.value) == 'The service_url is required'
867+
868+
869+
def test_service_url_slash():
870+
service = AnyServiceV1('2018-11-20', service_url='/',
871+
authenticator=NoAuthAuthenticator())
872+
assert service.service_url == ''
873+
with pytest.raises(ValueError) as err:
874+
service.prepare_request('POST',
875+
url='/',
876+
headers={'X-opt-out': True},
877+
data={'hello': 'world'})
878+
assert str(err.value) == 'The service_url is required'
863879

864880

865881
def test_service_url_not_set():

test/test_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ibm_cloud_sdk_core import get_query_param
2929
from ibm_cloud_sdk_core import read_external_sources
3030
from ibm_cloud_sdk_core.authenticators import Authenticator, BasicAuthenticator, IAMAuthenticator
31+
from ibm_cloud_sdk_core.utils import strip_extra_slashes
3132

3233

3334
def datetime_test(datestr: str, expected: str):
@@ -616,3 +617,18 @@ def test_read_external_sources_2():
616617

617618
config = read_external_sources('service_1')
618619
assert config.get('URL') == 'service1.com/api'
620+
621+
622+
def test_strip_extra_slashes():
623+
assert strip_extra_slashes('') == ''
624+
assert strip_extra_slashes('//') == '/'
625+
assert strip_extra_slashes('/////') == '/'
626+
assert strip_extra_slashes('https://host') == 'https://host'
627+
assert strip_extra_slashes('https://host/') == 'https://host/'
628+
assert strip_extra_slashes('https://host//') == 'https://host/'
629+
assert strip_extra_slashes('https://host/path') == 'https://host/path'
630+
assert strip_extra_slashes('https://host/path/') == 'https://host/path/'
631+
assert strip_extra_slashes('https://host/path//') == 'https://host/path/'
632+
assert strip_extra_slashes('https://host//path//') == 'https://host//path/'
633+
assert strip_extra_slashes(
634+
'https://host//path//////////') == 'https://host//path/'

0 commit comments

Comments
 (0)