diff --git a/src/DurableTask.AzureStorage/MessageManager.cs b/src/DurableTask.AzureStorage/MessageManager.cs index 06ac335b1..a1fc796cf 100644 --- a/src/DurableTask.AzureStorage/MessageManager.cs +++ b/src/DurableTask.AzureStorage/MessageManager.cs @@ -255,7 +255,9 @@ private async Task DownloadAndDecompressAsBytesAsync(Blob blob, Cancella public string GetBlobUrl(string blobName) { - return Uri.UnescapeDataString(this.blobContainer.GetBlobReference(blobName).Uri.AbsoluteUri); + string baseUri = this.blobContainer.GetBlobContainerUri().ToString(); + + return $"{baseUri}/{EscapeBlobNamePreservingSlashes(blobName)}"; } public MessageFormatFlags GetMessageFormatFlags(MessageData messageData) @@ -304,6 +306,12 @@ public async Task DeleteLargeMessageBlobs(string sanitizedInstanceId, Cance return storageOperationCount; } + + /// Escapes a blob name for safe inclusion in a URI while preserving path structure. + static string EscapeBlobNamePreservingSlashes(string blobName) + { + return string.Join("/", blobName.Split('/').Select(Uri.EscapeDataString)); + } } #if NETSTANDARD2_0 diff --git a/src/DurableTask.AzureStorage/Storage/BlobContainer.cs b/src/DurableTask.AzureStorage/Storage/BlobContainer.cs index 4abb9c61c..a61ac9f5b 100644 --- a/src/DurableTask.AzureStorage/Storage/BlobContainer.cs +++ b/src/DurableTask.AzureStorage/Storage/BlobContainer.cs @@ -108,6 +108,11 @@ public Task RenewLeaseAsync(string leaseId, CancellationToken cancellationToken .DecorateFailure(); } + public Uri GetBlobContainerUri() + { + return this.blobContainerClient.Uri; + } + static bool IsHnsFolder(BlobItem item) { // Check the optional "hdi_isfolder" value in the metadata to determine whether diff --git a/test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs b/test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs index c115c127d..a66ce6695 100644 --- a/test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs +++ b/test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs @@ -1600,6 +1600,35 @@ await ValidateLargeMessageBlobUrlAsync( } } + /// + /// End-to-end test that validates large (>60KB) messages stored in blob storage can be retrieved successfully, + /// when the instance ID includes special characters like '|' that can affect blob URL encoding. + /// + [TestMethod] + public async Task LargeMessage_WithEscapedInstanceId_CanBeStoredAndFetchedSuccessfully() + { + // Genereates a random large message. + const int largeMessageSize = 60 * 1024; + + using (TestOrchestrationHost host = TestHelpers.GetTestOrchestrationHost(enableExtendedSessions: false)) + { + await host.StartAsync(); + + string message = this.GenerateMediumRandomStringPayload(largeMessageSize, utf8ByteSize: 1, utf16ByteSize: 2).ToString(); + + // Use an instanceId that contains special characters which must be escaped in URIs + string id = "test|123:with white spcae"; + var client = await host.StartOrchestrationAsync(typeof(Orchestrations.Echo), input:message, instanceId: id); + var status = await client.WaitForCompletionAsync(TimeSpan.FromMinutes(2)); + + Assert.AreEqual(OrchestrationStatus.Completed, status?.OrchestrationStatus); + + // Verify that the output matches the original message (the blob was successfully downloaded and not returned as a URL) + StringAssert.Contains(status.Output.ToString(), message); + await host.StopAsync(); + } + } + [TestMethod] public async Task TagsAreAvailableInOrchestrationState() { diff --git a/test/DurableTask.AzureStorage.Tests/MessageManagerTests.cs b/test/DurableTask.AzureStorage.Tests/MessageManagerTests.cs index e7400d5a6..008a9eac7 100644 --- a/test/DurableTask.AzureStorage.Tests/MessageManagerTests.cs +++ b/test/DurableTask.AzureStorage.Tests/MessageManagerTests.cs @@ -13,12 +13,12 @@ #nullable enable namespace DurableTask.AzureStorage.Tests { - using System; - using System.Collections.Generic; using DurableTask.AzureStorage.Storage; using DurableTask.Core.History; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; + using System; + using System.Collections.Generic; [TestClass] public class MessageManagerTests @@ -70,10 +70,10 @@ public void DeserializesCustomTypes() } [DataTestMethod] - [DataRow("blob.bin")] - [DataRow("@#$%!")] - [DataRow("foo/bar/b@z.tar.gz")] - public void GetBlobUrlUnescaped(string blob) + [DataRow("blob.bin", "blob.bin")] + [DataRow("@#$%!", "%40%23%24%25%21")] + [DataRow("foo/bar/b@z.tar.gz", "foo/bar/b%40z.tar.gz")] + public void GetBlobUrlEscaped(string blob, string blobUrl) { var settings = new AzureStorageOrchestrationServiceSettings { @@ -82,7 +82,8 @@ public void GetBlobUrlUnescaped(string blob) const string container = "@entity12345"; var manager = new MessageManager(settings, new AzureStorageClient(settings), container); - var expected = $"http://127.0.0.1:10000/devstoreaccount1/{container}/{blob}"; + + var expected = $"http://127.0.0.1:10000/devstoreaccount1/{container}/{blobUrl}"; Assert.AreEqual(expected, manager.GetBlobUrl(blob)); }