Skip to content

Commit 54d16e6

Browse files
authored
Storage - Encoding Container Names (Azure#47093)
* implementation and some tests * wip * batch api tests * container api tests * wip * wrapping up helper tests * adding blob tests * adding datalake tests * fixing azurite failure * reverting parseNonIpUrl change * updating recordings and resolving errors * removing unecessary encode * addressing comments wip * wip * wip * addressing comments * adding onelake compatibility test * making options bag test more official * swapping onelake test to live only * removing onelake test * addressing last couple of comments
1 parent 520513d commit 54d16e6

33 files changed

+421
-73
lines changed

sdk/storage/azure-storage-blob-batch/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
### Breaking Changes
88

99
### Bugs Fixed
10+
- Fixed an issue where `BlobBatchSetBlobAccessTierOptions` would not properly handle blob names with special characters.
1011

1112
### Other Changes
13+
- Added support for container names with special characters when using OneLake.
1214

1315
## 12.28.0 (2025-10-21)
1416

sdk/storage/azure-storage-blob-batch/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "java",
44
"TagPrefix": "java/storage/azure-storage-blob-batch",
5-
"Tag": "java/storage/azure-storage-blob-batch_469f26eff9"
5+
"Tag": "java/storage/azure-storage-blob-batch_1951fdc3fd"
66
}

sdk/storage/azure-storage-blob-batch/src/main/java/com/azure/storage/blob/batch/BlobBatch.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,14 @@ public final class BlobBatch {
125125
* <!-- end com.azure.storage.blob.batch.BlobBatch.deleteBlob#String-String -->
126126
*
127127
* @param containerName The container of the blob.
128-
* @param blobName The name of the blob.
128+
* @param blobName The name of the blob. If the blob name contains special characters, it should be URL encoded.
129129
* @return a {@link Response} that will be used to associate this operation to the response when the batch is
130130
* submitted.
131131
* @throws UnsupportedOperationException If this batch has already added an operation of another type.
132132
*/
133133
public Response<Void> deleteBlob(String containerName, String blobName) {
134-
return deleteBlobHelper(containerName + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), null, null);
134+
return deleteBlobHelper(Utility.urlEncode(containerName) + "/" + Utility.urlEncode(Utility.urlDecode(blobName)),
135+
null, null);
135136
}
136137

137138
/**
@@ -149,7 +150,7 @@ public Response<Void> deleteBlob(String containerName, String blobName) {
149150
* <!-- end com.azure.storage.blob.batch.BlobBatch.deleteBlob#String-String-DeleteSnapshotsOptionType-BlobRequestConditions -->
150151
*
151152
* @param containerName The container of the blob.
152-
* @param blobName The name of the blob.
153+
* @param blobName The name of the blob. If the blob name contains special characters, it should be URL encoded.
153154
* @param deleteOptions Delete options for the blob and its snapshots.
154155
* @param blobRequestConditions Additional access conditions that must be met to allow this operation.
155156
* @return a {@link Response} that will be used to associate this operation to the response when the batch is
@@ -158,8 +159,8 @@ public Response<Void> deleteBlob(String containerName, String blobName) {
158159
*/
159160
public Response<Void> deleteBlob(String containerName, String blobName, DeleteSnapshotsOptionType deleteOptions,
160161
BlobRequestConditions blobRequestConditions) {
161-
return deleteBlobHelper(containerName + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), deleteOptions,
162-
blobRequestConditions);
162+
return deleteBlobHelper(Utility.urlEncode(containerName) + "/" + Utility.urlEncode(Utility.urlDecode(blobName)),
163+
deleteOptions, blobRequestConditions);
163164
}
164165

165166
/**
@@ -227,15 +228,16 @@ private Response<Void> deleteBlobHelper(String urlPath, DeleteSnapshotsOptionTyp
227228
* <!-- end com.azure.storage.blob.batch.BlobBatch.setBlobAccessTier#String-String-AccessTier -->
228229
*
229230
* @param containerName The container of the blob.
230-
* @param blobName The name of the blob.
231+
* @param blobName The name of the blob. If the blob name contains special characters, it should be URL encoded.
231232
* @param accessTier The tier to set on the blob.
232233
* @return a {@link Response} that will be used to associate this operation to the response when the batch is
233234
* submitted.
234235
* @throws UnsupportedOperationException If this batch has already added an operation of another type.
235236
*/
236237
public Response<Void> setBlobAccessTier(String containerName, String blobName, AccessTier accessTier) {
237-
return setBlobAccessTierHelper(containerName + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), accessTier,
238-
null, null, null);
238+
return setBlobAccessTierHelper(
239+
Utility.urlEncode(containerName) + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), accessTier, null,
240+
null, null);
239241
}
240242

241243
/**
@@ -251,7 +253,7 @@ public Response<Void> setBlobAccessTier(String containerName, String blobName, A
251253
* <!-- end com.azure.storage.blob.batch.BlobBatch.setBlobAccessTier#String-String-AccessTier-String -->
252254
*
253255
* @param containerName The container of the blob.
254-
* @param blobName The name of the blob.
256+
* @param blobName The name of the blob. If the blob name contains special characters, it should be URL encoded.
255257
* @param accessTier The tier to set on the blob.
256258
* @param leaseId The lease ID the active lease on the blob must match.
257259
* @return a {@link Response} that will be used to associate this operation to the response when the batch is
@@ -260,8 +262,9 @@ public Response<Void> setBlobAccessTier(String containerName, String blobName, A
260262
*/
261263
public Response<Void> setBlobAccessTier(String containerName, String blobName, AccessTier accessTier,
262264
String leaseId) {
263-
return setBlobAccessTierHelper(containerName + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), accessTier,
264-
null, leaseId, null);
265+
return setBlobAccessTierHelper(
266+
Utility.urlEncode(containerName) + "/" + Utility.urlEncode(Utility.urlDecode(blobName)), accessTier, null,
267+
leaseId, null);
265268
}
266269

267270
/**

sdk/storage/azure-storage-blob-batch/src/main/java/com/azure/storage/blob/batch/options/BlobBatchSetBlobAccessTierOptions.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ public String getBlobName() {
8989
* @return Identifier of the blob to set its access tier.
9090
*/
9191
public String getBlobIdentifier() {
92-
String basePath = blobUrlParts.getBlobContainerName() + "/" + blobUrlParts.getBlobName();
92+
String basePath = Utility.urlEncode(blobUrlParts.getBlobContainerName()) + "/"
93+
+ Utility.urlEncode(blobUrlParts.getBlobName());
9394
String snapshot = blobUrlParts.getSnapshot();
9495
String versionId = blobUrlParts.getVersionId();
9596
if (snapshot != null && versionId != null) {

sdk/storage/azure-storage-blob-batch/src/test/java/com/azure/storage/blob/batch/BatchApiTests.java

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.azure.storage.blob.specialized.BlobClientBase;
2121
import com.azure.storage.blob.specialized.BlockBlobClient;
2222
import com.azure.storage.blob.specialized.PageBlobClient;
23+
import com.azure.storage.common.Utility;
2324
import com.azure.storage.common.sas.AccountSasPermission;
2425
import com.azure.storage.common.sas.AccountSasResourceType;
2526
import com.azure.storage.common.sas.AccountSasService;
@@ -794,4 +795,157 @@ public void submitBatchWithContainerSasCredentialsError() {
794795
= assertThrows(BlobBatchStorageException.class, () -> batchClient.submitBatch(batch));
795796
assertEquals(2, getIterableSize(ex.getBatchExceptions()));
796797
}
798+
799+
// Tests container name encoding for BlobBatch.deleteBlob. Container names with special characters are not supported
800+
// by the service, however, the names should still be encoded.
801+
@Test
802+
public void deleteBlobContainerNameEncoding() {
803+
String containerName = "my container";
804+
String blobName = generateBlobName();
805+
806+
BlobBatch batch = batchClient.getBlobBatch();
807+
Response<Void> response = batch.deleteBlob(containerName, blobName);
808+
809+
assertThrows(BlobBatchStorageException.class, () -> batchClient.submitBatch(batch));
810+
BlobStorageException temp = assertThrows(BlobStorageException.class, response::getRequest);
811+
812+
assertTrue(temp.getResponse().getRequest().getUrl().toString().contains("my%20container"));
813+
}
814+
815+
// Tests blob name encoding for BlobBatch.deleteBlob.
816+
@Test
817+
public void deleteBlobNameEncoding() {
818+
String containerName = generateContainerName();
819+
String blobName = generateBlobName() + "enc!";
820+
BlobContainerClient containerClient = primaryBlobServiceClient.createBlobContainer(containerName);
821+
containerClient.getBlobClient(blobName).getPageBlobClient().create(0);
822+
823+
BlobBatch batch = batchClient.getBlobBatch();
824+
Response<Void> response = batch.deleteBlob(containerName, blobName);
825+
batchClient.submitBatch(batch);
826+
827+
assertEquals(202, response.getStatusCode());
828+
}
829+
830+
// Tests container name encoding for BlobBatch.setBlobAccessTier. Container names with special characters are not supported
831+
// by the service, however, the names should still be encoded.
832+
@Test
833+
public void setTierContainerNameEncoding() {
834+
String containerName = "my container";
835+
String blobName = generateBlobName();
836+
837+
BlobBatch batch = batchClient.getBlobBatch();
838+
Response<Void> response = batch.setBlobAccessTier(containerName, blobName, AccessTier.HOT);
839+
840+
assertThrows(BlobBatchStorageException.class, () -> batchClient.submitBatch(batch));
841+
BlobStorageException temp = assertThrows(BlobStorageException.class, response::getRequest);
842+
843+
assertTrue(temp.getResponse().getRequest().getUrl().toString().contains("my%20container"));
844+
}
845+
846+
// Tests blob name encoding for BlobBatch.setBlobAccessTier
847+
@Test
848+
public void setTierBlobNameEncoding() {
849+
String containerName = generateContainerName();
850+
String blobName = generateBlobName() + "enc!";
851+
BlobContainerClient containerClient = primaryBlobServiceClient.createBlobContainer(containerName);
852+
containerClient.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultBinaryData());
853+
854+
BlobBatch batch = batchClient.getBlobBatch();
855+
Response<Void> response = batch.setBlobAccessTier(containerName, blobName, AccessTier.HOT);
856+
batchClient.submitBatch(batch);
857+
858+
assertEquals(200, response.getStatusCode());
859+
}
860+
861+
// Tests container name encoding for BlobBatchSetBlobAccessTierOptions constructor. Container names with special characters are not supported
862+
// by the service, however, the names should still be encoded.
863+
@Test
864+
public void setTierContainerNameEncodingOptionsConstructor() {
865+
String containerName = "my container";
866+
String blobName = generateBlobName();
867+
868+
BlobBatch batch = batchClient.getBlobBatch();
869+
BlobBatchSetBlobAccessTierOptions options
870+
= new BlobBatchSetBlobAccessTierOptions(containerName, blobName, AccessTier.HOT);
871+
Response<Void> response = batch.setBlobAccessTier(options);
872+
873+
assertThrows(BlobBatchStorageException.class, () -> batchClient.submitBatch(batch));
874+
BlobStorageException temp = assertThrows(BlobStorageException.class, response::getRequest);
875+
876+
assertTrue(temp.getResponse().getRequest().getUrl().toString().contains("my%20container"));
877+
}
878+
879+
//Tests blob name encoding for BlobBatchSetBlobAccessTierOptions constructor
880+
@Test
881+
public void setTierBlobNameEncodingOptionsConstructor() {
882+
String containerName = generateContainerName();
883+
String blobName = generateBlobName() + "enc!";
884+
BlobContainerClient containerClient = primaryBlobServiceClient.createBlobContainer(containerName);
885+
containerClient.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultBinaryData());
886+
887+
BlobBatch batch = batchClient.getBlobBatch();
888+
BlobBatchSetBlobAccessTierOptions options
889+
= new BlobBatchSetBlobAccessTierOptions(containerName, blobName, AccessTier.HOT);
890+
Response<Void> response = batch.setBlobAccessTier(options);
891+
batchClient.submitBatch(batch);
892+
893+
assertEquals(200, response.getStatusCode());
894+
String identifier = options.getBlobIdentifier();
895+
assertTrue(identifier.contains(Utility.urlEncode(blobName)));
896+
}
897+
898+
// Tests getters return unencoded names (constructor with separate names)
899+
@Test
900+
public void getBlobNameAndContainerNameOptionsConstructor() {
901+
String containerName = "my container";
902+
String blobName = "my blob";
903+
904+
BlobBatchSetBlobAccessTierOptions options
905+
= new BlobBatchSetBlobAccessTierOptions(containerName, blobName, AccessTier.HOT);
906+
907+
assertEquals(containerName, options.getBlobContainerName());
908+
assertEquals(blobName, options.getBlobName());
909+
910+
String identifier = options.getBlobIdentifier();
911+
assertTrue(identifier.contains("my%20container"));
912+
assertTrue(identifier.contains("my%20blob"));
913+
}
914+
915+
// Tests getters return unencoded names (constructor with full blob URL)
916+
@Test
917+
public void getBlobNameAndContainerNameUrlConstructor() {
918+
String containerName = "my container";
919+
String blobName = "my blob";
920+
BlockBlobClient blockBlobClient = primaryBlobServiceClient.getBlobContainerClient(containerName)
921+
.getBlobClient(blobName)
922+
.getBlockBlobClient();
923+
924+
BlobBatchSetBlobAccessTierOptions options
925+
= new BlobBatchSetBlobAccessTierOptions(blockBlobClient.getBlobUrl(), AccessTier.HOT);
926+
927+
assertEquals(containerName, options.getBlobContainerName());
928+
assertEquals(blobName, options.getBlobName());
929+
930+
String identifier = options.getBlobIdentifier();
931+
assertTrue(identifier.contains("my%20container"));
932+
assertTrue(identifier.contains("my%20blob"));
933+
}
934+
935+
@Test
936+
public void blobBatchSetBlobAccessTierOptionsHandlesSpecialChars() {
937+
String blobName = "my blob";
938+
String containerName = generateContainerName();
939+
940+
BlobBatch batch = batchClient.getBlobBatch();
941+
BlobContainerClient containerClient = primaryBlobServiceClient.createBlobContainer(containerName);
942+
943+
BlobClient blobClient1 = containerClient.getBlobClient(blobName);
944+
blobClient1.getBlockBlobClient().upload(DATA.getDefaultBinaryData());
945+
946+
Response<Void> response
947+
= batch.setBlobAccessTier(new BlobBatchSetBlobAccessTierOptions(containerName, blobName, AccessTier.HOT));
948+
batchClient.submitBatch(batch);
949+
assertEquals(200, response.getStatusCode());
950+
}
797951
}

sdk/storage/azure-storage-blob/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Bugs Fixed
1010

1111
### Other Changes
12+
- Added support for container names with special characters when using OneLake.
1213

1314
## 12.32.0 (2025-10-21)
1415

sdk/storage/azure-storage-blob/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "java",
44
"TagPrefix": "java/storage/azure-storage-blob",
5-
"Tag": "java/storage/azure-storage-blob_16534d98da"
5+
"Tag": "java/storage/azure-storage-blob_c018337a13"
66
}

sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import com.azure.storage.blob.options.FindBlobsOptions;
5151
import com.azure.storage.blob.sas.BlobServiceSasSignatureValues;
5252
import com.azure.storage.common.StorageSharedKeyCredential;
53+
import com.azure.storage.common.Utility;
5354
import com.azure.storage.common.implementation.SasImplUtils;
5455
import com.azure.storage.common.implementation.StorageImplUtils;
5556
import reactor.core.publisher.Mono;
@@ -220,7 +221,7 @@ public String getAccountUrl() {
220221
* @return the URL.
221222
*/
222223
public String getBlobContainerUrl() {
223-
return azureBlobStorage.getUrl() + "/" + containerName;
224+
return azureBlobStorage.getUrl() + "/" + Utility.urlEncode(containerName);
224225
}
225226

226227
/**

sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import com.azure.storage.blob.options.FindBlobsOptions;
5454
import com.azure.storage.blob.sas.BlobServiceSasSignatureValues;
5555
import com.azure.storage.common.StorageSharedKeyCredential;
56+
import com.azure.storage.common.Utility;
5657
import com.azure.storage.common.implementation.SasImplUtils;
5758
import com.azure.storage.common.implementation.StorageImplUtils;
5859

@@ -232,7 +233,7 @@ public String getAccountUrl() {
232233
* @return the URL.
233234
*/
234235
public String getBlobContainerUrl() {
235-
return azureBlobStorage.getUrl() + "/" + containerName;
236+
return azureBlobStorage.getUrl() + "/" + Utility.urlEncode(containerName);
236237
}
237238

238239
/**

sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobUrlParts.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,27 +110,40 @@ public BlobUrlParts setHost(String host) {
110110
}
111111

112112
/**
113-
* Gets the container name that will be used as part of the URL path.
113+
* Gets the decoded container name that will be used as part of the URL path.
114+
* <p> Note:
115+
* This value may differ from the original value provided to {@link #setContainerName(String)}
116+
* because the setter and getter do not guarantee round-trip consistency.
117+
* This behavior is intentional to normalize names that may or may not be URL encoded. </p>
114118
*
115-
* @return the container name.
119+
* @return the decoded container name.
116120
*/
117121
public String getBlobContainerName() {
118-
return containerName;
122+
return (containerName == null) ? null : Utility.urlDecode(containerName);
119123
}
120124

121125
/**
122126
* Sets the container name that will be used as part of the URL path.
127+
* <p> Note:
128+
* The setter and getter do not guarantee round-trip consistency.
129+
* This is because container names with special characters may need URL encoding,
130+
* and this method normalizes the input by decoding and then encoding it.
131+
* If the container name contains special characters, it is recommended to URL encode it. </p>
123132
*
124-
* @param containerName The container nme.
133+
* @param containerName The container name. If the container name contains special characters, it should be URL encoded.
125134
* @return the updated BlobUrlParts object.
126135
*/
127136
public BlobUrlParts setContainerName(String containerName) {
128-
this.containerName = containerName;
137+
this.containerName = Utility.urlEncode(Utility.urlDecode(containerName));
129138
return this;
130139
}
131140

132141
/**
133-
* Decodes and gets the blob name that will be used as part of the URL path.
142+
* Gets the decoded blob name that will be used as part of the URL path.
143+
* <p> Note:
144+
* This value may differ from the original value provided to {@link #setBlobName(String)}
145+
* because the setter and getter do not guarantee round-trip consistency.
146+
* This behavior is intentional to normalize names that may or may not be URL encoded. </p>
134147
*
135148
* @return the decoded blob name.
136149
*/
@@ -140,9 +153,13 @@ public String getBlobName() {
140153

141154
/**
142155
* Sets the blob name that will be used as part of the URL path.
156+
* <p> Note:
157+
* The setter and getter do not guarantee round-trip consistency.
158+
* This is because blob names with special characters may need URL encoding,
159+
* and this method normalizes the input by decoding and then encoding it.
160+
* If the blob name contains special characters, it is recommended to URL encode it. </p>
143161
*
144-
* @param blobName The blob name. If the blob name contains special characters, pass in the url encoded version
145-
* of the blob name.
162+
* @param blobName The blob name. If the blob name contains special characters, it should be URL encoded.
146163
* @return the updated BlobUrlParts object.
147164
*/
148165
public BlobUrlParts setBlobName(String blobName) {

0 commit comments

Comments
 (0)