Skip to content

Commit ae93966

Browse files
authored
Fix bug with container name with spaces (#42608)
* fix bug with container name with spaces * update changelog * fix test * fix tests * remove unused line
1 parent 674ee6c commit ae93966

File tree

4 files changed

+70
-9
lines changed

4 files changed

+70
-9
lines changed

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#### Bugs Fixed
1111
* Improved the resilience of Database Account Read metadata operation against short-lived network issues by increasing number of retries. See [PR 42525](https://github.com/Azure/azure-sdk-for-python/pull/42525).
1212
* Fixed bug where during health checks read regions were marked as unavailable for write operations. See [PR 42525](https://github.com/Azure/azure-sdk-for-python/pull/42525).
13+
* Fixed bug where containers named with spaces or special characters using session consistency would fall back to eventual consistency. See [PR 42608](https://github.com/Azure/azure-sdk-for-python/pull/42608)
1314
* Fixed bug where `excluded_locations` was not being honored for some metadata calls. See [PR 42266](https://github.com/Azure/azure-sdk-for-python/pull/42266).
1415

1516
#### Other Changes

sdk/cosmos/azure-cosmos/azure/cosmos/_base.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from typing import Dict, Any, List, Mapping, Optional, Sequence, Union, Tuple, TYPE_CHECKING
3232

3333
from urllib.parse import quote as urllib_quote
34+
from urllib.parse import unquote as urllib_unquote
3435
from urllib.parse import urlsplit
3536
from azure.core import MatchConditions
3637

@@ -358,6 +359,8 @@ def set_session_token_header(
358359
# then update from session container
359360
if headers[http_constants.HttpHeaders.ConsistencyLevel] == documents.ConsistencyLevel.Session and \
360361
cosmos_client_connection.session:
362+
# urllib_unquote is used to decode the path, as it may contain encoded characters
363+
path = urllib_unquote(path)
361364
# populate session token from the client's session container
362365
session_token = (
363366
cosmos_client_connection.session.get_session_token(path,
@@ -387,6 +390,8 @@ async def set_session_token_header_async(
387390
if headers[http_constants.HttpHeaders.ConsistencyLevel] == documents.ConsistencyLevel.Session and \
388391
cosmos_client_connection.session:
389392
# populate session token from the client's session container
393+
# urllib_unquote is used to decode the path, as it may contain encoded characters
394+
path = urllib_unquote(path)
390395
session_token = \
391396
await cosmos_client_connection.session.get_session_token_async(path,
392397
options.get('partitionKey'),
@@ -608,19 +613,21 @@ def GetItemContainerInfo(self_link: str, alt_content_path: str, resource_id: str
608613

609614
self_link = TrimBeginningAndEndingSlashes(self_link) + "/"
610615

611-
index = IndexOfNth(self_link, "/", 4)
616+
end_index = IndexOfNth(self_link, "/", 4)
617+
start_index = IndexOfNth(self_link, "/", 3)
612618

613-
if index != -1:
614-
collection_id = self_link[0:index]
619+
if start_index != -1 and end_index != -1:
620+
# parse only the collection rid from the path as it's unique across databases
621+
collection_rid = self_link[start_index + 1:end_index]
615622

616623
if "colls" in self_link:
617624
# this is a collection request
618625
index_second_slash = IndexOfNth(alt_content_path, "/", 2)
619626
if index_second_slash == -1:
620627
collection_name = alt_content_path + "/colls/" + urllib_quote(resource_id)
621-
return collection_id, collection_name
628+
return collection_rid, collection_name
622629
collection_name = alt_content_path
623-
return collection_id, collection_name
630+
return collection_rid, collection_name
624631
raise ValueError(
625632
"Response Not from Server Partition, self_link: {0}, alt_content_path: {1}, id: {2}".format(
626633
self_link, alt_content_path, resource_id

sdk/cosmos/azure-cosmos/tests/test_session.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,36 @@ def test_session_token_sm_for_ops(self):
8686
assert self.created_db.client_connection.last_response_headers.get(HttpHeaders.SessionToken) is not None
8787
assert self.created_db.client_connection.last_response_headers.get(HttpHeaders.SessionToken) != batch_response_token
8888

89+
def test_session_token_with_space_in_container_name(self):
90+
91+
# Session token should not be sent for control plane operations
92+
test_container = self.created_db.create_container(
93+
"Container with space" + str(uuid.uuid4()),
94+
PartitionKey(path="/pk"),
95+
raw_response_hook=test_config.no_token_response_hook
96+
)
97+
try:
98+
# Session token should be sent for document read/batch requests only - verify it is not sent for write requests
99+
created_document = test_container.create_item(body={'id': '1' + str(uuid.uuid4()), 'pk': 'mypk'},
100+
raw_response_hook=test_config.no_token_response_hook)
101+
response_session_token = created_document.get_response_headers().get(HttpHeaders.SessionToken)
102+
read_item = test_container.read_item(item=created_document['id'], partition_key='mypk',
103+
raw_response_hook=test_config.token_response_hook)
104+
query_iterable = test_container.query_items(
105+
"SELECT * FROM c WHERE c.id = '" + str(created_document['id']) + "'",
106+
partition_key='mypk',
107+
raw_response_hook=test_config.token_response_hook)
108+
for _ in query_iterable:
109+
pass
110+
111+
assert (read_item.get_response_headers().get(HttpHeaders.SessionToken) ==
112+
response_session_token)
113+
finally:
114+
self.created_db.delete_container(test_container)
115+
89116
def test_session_token_mwr_for_ops(self):
90117
# For multiple write regions, all document requests should send out session tokens
91118
# We will use fault injection to simulate the regions the emulator needs
92-
first_region_uri: str = test_config.TestConfig.local_host.replace("localhost", "127.0.0.1")
93119
custom_transport = FaultInjectionTransport()
94120

95121
# Inject topology transformation that would make Emulator look like a multiple write region account

sdk/cosmos/azure-cosmos/tests/test_session_async.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from _fault_injection_transport_async import FaultInjectionTransportAsync
1111
import azure.cosmos.exceptions as exceptions
1212
import test_config
13-
from azure.cosmos.aio import CosmosClient, _retry_utility_async
14-
from azure.cosmos import DatabaseProxy, PartitionKey
13+
from azure.cosmos.aio import CosmosClient, _retry_utility_async, DatabaseProxy
14+
from azure.cosmos import PartitionKey
1515
from azure.cosmos.http_constants import StatusCodes, SubStatusCodes, HttpHeaders
1616
from azure.core.pipeline.transport._aiohttp import AioHttpTransportResponse
1717
from azure.core.rest import HttpRequest, AsyncHttpResponse
@@ -87,10 +87,37 @@ async def test_session_token_swr_for_ops_async(self):
8787
assert self.created_db.client_connection.last_response_headers.get(HttpHeaders.SessionToken) is not None
8888
assert self.created_db.client_connection.last_response_headers.get(HttpHeaders.SessionToken) != batch_response_token
8989

90+
async def test_session_token_with_space_in_container_name_async(self):
91+
92+
# Session token should not be sent for control plane operations
93+
test_container = await self.created_db.create_container(
94+
"Container with space" + str(uuid.uuid4()),
95+
PartitionKey(path="/pk"),
96+
raw_response_hook=test_config.no_token_response_hook
97+
)
98+
try:
99+
# Session token should be sent for document read/batch requests only - verify it is not sent for write requests
100+
created_document = await test_container.create_item(body={'id': '1' + str(uuid.uuid4()), 'pk': 'mypk'},
101+
raw_response_hook=test_config.no_token_response_hook)
102+
response_session_token = created_document.get_response_headers().get(HttpHeaders.SessionToken)
103+
read_item = await test_container.read_item(item=created_document['id'], partition_key='mypk',
104+
raw_response_hook=test_config.token_response_hook)
105+
query_iterable = test_container.query_items(
106+
"SELECT * FROM c WHERE c.id = '" + str(created_document['id']) + "'",
107+
partition_key='mypk',
108+
raw_response_hook=test_config.token_response_hook)
109+
110+
async for _ in query_iterable:
111+
pass
112+
113+
assert (read_item.get_response_headers().get(HttpHeaders.SessionToken) ==
114+
response_session_token)
115+
finally:
116+
await self.created_db.delete_container(test_container)
117+
90118
async def test_session_token_mwr_for_ops_async(self):
91119
# For multiple write regions, all document requests should send out session tokens
92120
# We will use fault injection to simulate the regions the emulator needs
93-
first_region_uri: str = test_config.TestConfig.local_host.replace("localhost", "127.0.0.1")
94121
custom_transport = FaultInjectionTransportAsync()
95122

96123
# Inject topology transformation that would make Emulator look like a multiple write region account

0 commit comments

Comments
 (0)