diff --git a/sdk/storage/Azure.Storage.Blobs/assets.json b/sdk/storage/Azure.Storage.Blobs/assets.json index 4efeef6ef9fb..ed50b4446fc0 100644 --- a/sdk/storage/Azure.Storage.Blobs/assets.json +++ b/sdk/storage/Azure.Storage.Blobs/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.Blobs", - "Tag": "net/storage/Azure.Storage.Blobs_9497f4a828" + "Tag": "net/storage/Azure.Storage.Blobs_62a8c8fa29" } diff --git a/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs b/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs index 564a20a82d6e..a0269ba6c60d 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs @@ -306,6 +306,8 @@ private void AddHeadersAndQueryParameters() Diagnostics.LoggedHeaderNames.Add("x-ms-source-if-unmodified-since"); Diagnostics.LoggedHeaderNames.Add("x-ms-tag-count"); Diagnostics.LoggedHeaderNames.Add("x-ms-encryption-key-sha256"); + Diagnostics.LoggedHeaderNames.Add("x-ms-copy-source-error-code"); + Diagnostics.LoggedHeaderNames.Add("x-ms-copy-source-status-code"); Diagnostics.LoggedQueryParameters.Add("comp"); Diagnostics.LoggedQueryParameters.Add("maxresults"); diff --git a/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs index 40c7fa099714..7fae438be11d 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs @@ -1317,6 +1317,27 @@ public async Task AppendBlockFromUriAsync_Min() } } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2024_08_04)] + public async Task AppendBlockFromUriAsync_SourceErrorAndStatusCode() + { + await using DisposingContainer test = await GetTestContainerAsync(publicAccessType: PublicAccessType.None); + + AppendBlobClient sourceBlob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + AppendBlobClient destBlob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName())); + await destBlob.CreateIfNotExistsAsync(); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + destBlob.AppendBlockFromUriAsync(sourceBlob.Uri), + e => + { + Assert.IsTrue(e.Message.Contains("CopySourceStatusCode: 409")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorCode: PublicAccessNotPermitted")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorMessage: Public access is not permitted on this storage account.")); + }); + } + [RecordedTest] [TestCase(nameof(AppendBlobRequestConditions.LeaseId))] [TestCase(nameof(AppendBlobRequestConditions.TagConditions))] diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs index 10ba5249f161..ef231d3cea1e 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs @@ -1999,6 +1999,29 @@ public async Task StartCopyFromUriAsync() Assert.IsTrue(operation.HasValue); } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2024_08_04)] + public async Task StartCopyFromUriAsync_SourceErrorAndStatusCode() + { + await using DisposingContainer test = await GetTestContainerAsync(publicAccessType: PublicAccessType.None); + + // Arrange + BlobBaseClient srcBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + BlockBlobClient destBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + + Uri sourceUri = srcBlob.GenerateSasUri(BlobSasPermissions.Read, GetUtcNow().AddDays(1)); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + destBlob.StartCopyFromUriAsync(sourceUri), + e => + { + Assert.IsTrue(e.Message.Contains("CopySourceStatusCode: 404")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorCode: BlobNotFound")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorMessage: The specified blob does not exist.")); + }); + } + [RecordedTest] [TestCase(nameof(BlobRequestConditions.LeaseId))] public async Task StartCopyFromUriAsync_InvalidSourceRequestConditions(string invalidSourceCondition) @@ -3332,6 +3355,27 @@ await destBlob.SyncCopyFromUriAsync( } } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2024_08_04)] + public async Task SyncCopyFromUriAsync_SourceErrorAndStatusCode() + { + await using DisposingContainer test = await GetTestContainerAsync(publicAccessType: PublicAccessType.None); + + // Arrange + BlockBlobClient srcBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + BlockBlobClient destBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + destBlob.SyncCopyFromUriAsync(srcBlob.Uri), + e => + { + Assert.IsTrue(e.Message.Contains("CopySourceStatusCode: 409")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorCode: PublicAccessNotPermitted")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorMessage: Public access is not permitted on this storage account.")); + }); + } + [RecordedTest] public async Task DeleteAsync() { diff --git a/sdk/storage/Azure.Storage.Blobs/tests/BlockBlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/BlockBlobClientTests.cs index 781ebefa35e4..bc4b864e897d 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/BlockBlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/BlockBlobClientTests.cs @@ -614,6 +614,27 @@ await RetryAsync( _retryStageBlockFromUri); } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2024_08_04)] + public async Task StageBlobFromUriAsync_SourceErrorAndStatusCode() + { + // Arrange + var constants = TestConstants.Create(this); + await using DisposingContainer test = await GetTestContainerAsync(publicAccessType: PublicAccessType.None); + BlockBlobClient sourceBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + BlockBlobClient destBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + destBlob.StageBlockFromUriAsync(sourceBlob.Uri, ToBase64(GetNewBlockName())), + e => + { + Assert.IsTrue(e.Message.Contains("CopySourceStatusCode: 409")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorCode: PublicAccessNotPermitted")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorMessage: Public access is not permitted on this storage account.")); + }); + } + [RecordedTest] [TestCase(nameof(BlobRequestConditions.IfModifiedSince))] [TestCase(nameof(BlobRequestConditions.IfUnmodifiedSince))] @@ -2821,6 +2842,27 @@ await TestHelper.AssertExpectedExceptionAsync( e => Assert.AreEqual(BlobErrorCode.CannotVerifyCopySource.ToString(), e.ErrorCode)); } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2024_08_04)] + public async Task SyncUploadFromUriAsync_SourceErrorAndStatusCode() + { + // Arrange + var constants = TestConstants.Create(this); + await using DisposingContainer test = await GetTestContainerAsync(publicAccessType: PublicAccessType.None); + BlockBlobClient sourceBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + BlockBlobClient destBlob = InstrumentClient(test.Container.GetBlockBlobClient(GetNewBlobName())); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + destBlob.SyncUploadFromUriAsync(sourceBlob.Uri), + e => + { + Assert.IsTrue(e.Message.Contains("CopySourceStatusCode: 409")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorCode: PublicAccessNotPermitted")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorMessage: Public access is not permitted on this storage account.")); + }); + } + [RecordedTest] [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2020_04_08)] public async Task SyncUploadFromUriAsync_OverwriteSourceBlobProperties() diff --git a/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs b/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs index a7a1007a5d68..419e638fa417 100644 --- a/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs +++ b/sdk/storage/Azure.Storage.Blobs/tests/PageBlobClientTests.cs @@ -3370,6 +3370,31 @@ public async Task UploadPagesFromUriAsync_Min() } } + [RecordedTest] + [ServiceVersion(Min = BlobClientOptions.ServiceVersion.V2024_08_04)] + public async Task UploadPagesFromUriAsync_SourceErrorAndStatusCode() + { + // Arrange + await using DisposingContainer test = await GetTestContainerAsync(publicAccessType: PublicAccessType.None); + + PageBlobClient sourceBlob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + + PageBlobClient destBlob = InstrumentClient(test.Container.GetPageBlobClient(GetNewBlobName())); + await destBlob.CreateIfNotExistsAsync(Constants.KB); + + HttpRange range = new HttpRange(0, Constants.KB); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + destBlob.UploadPagesFromUriAsync(sourceBlob.Uri, range, range), + e => + { + Assert.IsTrue(e.Message.Contains("CopySourceStatusCode: 409")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorCode: PublicAccessNotPermitted")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorMessage: Public access is not permitted on this storage account.")); + }); + } + [RecordedTest] [TestCase(nameof(PageBlobRequestConditions.LeaseId))] [TestCase(nameof(PageBlobRequestConditions.IfSequenceNumberLessThanOrEqual))] diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs b/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs index c77053e393b7..006a03ce94fd 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/Constants.cs @@ -194,6 +194,7 @@ internal static class HeaderNames public const string LeaseId = "x-ms-lease-id"; public const string LastModified = "Last-Modified"; public const string ETag = "ETag"; + public const string CopySourceErrorCode = "x-ms-copy-source-error-code"; } internal static class ErrorCodes diff --git a/sdk/storage/Azure.Storage.Common/src/Shared/StorageResponseClassifier.cs b/sdk/storage/Azure.Storage.Common/src/Shared/StorageResponseClassifier.cs index 01c4dbdad644..e364faa7f251 100644 --- a/sdk/storage/Azure.Storage.Common/src/Shared/StorageResponseClassifier.cs +++ b/sdk/storage/Azure.Storage.Common/src/Shared/StorageResponseClassifier.cs @@ -41,6 +41,20 @@ public override bool IsRetriableResponse(HttpMessage message) return true; } } + + // Retry select Copy Source Error Codes. + if (message.Response.Status >= 400 && + message.Response.Headers.TryGetValue(Constants.HeaderNames.CopySourceErrorCode, out string copySourceError)) + { + switch (copySourceError) + { + case Constants.ErrorCodes.InternalError: + case Constants.ErrorCodes.OperationTimedOut: + case Constants.ErrorCodes.ServerBusy: + return true; + } + } + return base.IsRetriableResponse(message); } diff --git a/sdk/storage/Azure.Storage.Common/tests/StorageResponseClassifierTests.cs b/sdk/storage/Azure.Storage.Common/tests/StorageResponseClassifierTests.cs index d2bdb94a5ea3..dd6384c0d582 100644 --- a/sdk/storage/Azure.Storage.Common/tests/StorageResponseClassifierTests.cs +++ b/sdk/storage/Azure.Storage.Common/tests/StorageResponseClassifierTests.cs @@ -70,6 +70,30 @@ public void IsRetriableResponse_StorageErrors_SecondaryUri(string errorCode) Assert.IsTrue(classifier.IsRetriableResponse(message)); } + [Test] + [TestCase(Constants.ErrorCodes.ServerBusy)] + [TestCase(Constants.ErrorCodes.InternalError)] + [TestCase(Constants.ErrorCodes.OperationTimedOut)] + public void IsRetriableResponse_CopySourceErrors(string errorCode) + { + var response = new MockResponse(Constants.HttpStatusCode.NotFound); + response.AddHeader(new HttpHeader(Constants.HeaderNames.CopySourceErrorCode, errorCode)); + HttpMessage message = BuildMessage(response); + Assert.IsTrue(classifier.IsRetriableResponse(message)); + } + + [Test] + [TestCase(Constants.ErrorCodes.ServerBusy)] + [TestCase(Constants.ErrorCodes.InternalError)] + [TestCase(Constants.ErrorCodes.OperationTimedOut)] + public void IsRetriableResponse_CopySourceErrors_SecondaryUri(string errorCode) + { + var response = new MockResponse(Constants.HttpStatusCode.NotFound); + response.AddHeader(new HttpHeader(Constants.HeaderNames.CopySourceErrorCode, errorCode)); + HttpMessage message = BuildMessage(response, MockSecondaryUri); + Assert.IsTrue(classifier.IsRetriableResponse(message)); + } + [TestCase("ContainerAlreadyExists", "If-Match", false)] [TestCase("ContainerAlreadyExists","If-None-Match", false)] [TestCase("ContainerAlreadyExists","If-Unmodified-Since", false)] diff --git a/sdk/storage/Azure.Storage.Files.Shares/assets.json b/sdk/storage/Azure.Storage.Files.Shares/assets.json index cd4d747cd822..8f49e15e0259 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/assets.json +++ b/sdk/storage/Azure.Storage.Files.Shares/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.Files.Shares", - "Tag": "net/storage/Azure.Storage.Files.Shares_3a253f1460" + "Tag": "net/storage/Azure.Storage.Files.Shares_07c3d59f5a" } diff --git a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs index 50692f6910da..0600d6c55418 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/src/ShareClientOptions.cs @@ -264,6 +264,8 @@ private void AddHeadersAndQueryParameters() Diagnostics.LoggedHeaderNames.Add("x-ms-share-quota"); Diagnostics.LoggedHeaderNames.Add("x-ms-type"); Diagnostics.LoggedHeaderNames.Add("x-ms-write"); + Diagnostics.LoggedHeaderNames.Add("x-ms-copy-source-error-code"); + Diagnostics.LoggedHeaderNames.Add("x-ms-copy-source-status-code"); Diagnostics.LoggedQueryParameters.Add("comp"); Diagnostics.LoggedQueryParameters.Add("maxresults"); diff --git a/sdk/storage/Azure.Storage.Files.Shares/tests/FileClientTests.cs b/sdk/storage/Azure.Storage.Files.Shares/tests/FileClientTests.cs index 35fa0932958f..73abef739b68 100644 --- a/sdk/storage/Azure.Storage.Files.Shares/tests/FileClientTests.cs +++ b/sdk/storage/Azure.Storage.Files.Shares/tests/FileClientTests.cs @@ -1998,6 +1998,24 @@ await TestHelper.AssertExpectedExceptionAsync( e => Assert.AreEqual("CannotVerifyCopySource", e.ErrorCode)); } + [RecordedTest] + [ServiceVersion(Min = ShareClientOptions.ServiceVersion.V2024_08_04)] + public async Task StartCopyAsync_SourceErrorAndStatusCode() + { + await using DisposingFile test = await SharesClientBuilder.GetTestFileAsync(); + ShareFileClient file = test.File; + + // Act + await TestHelper.AssertExpectedExceptionAsync( + file.StartCopyAsync(sourceUri: s_invalidUri), + e => + { + Assert.IsTrue(e.Message.Contains("CopySourceStatusCode: 400")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorCode: InvalidQueryParameterValue")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorMessage: Value for one of the query parameters specified in the request URI is invalid.")); + }); + } + [RecordedTest] public async Task StartCopyAsync_CopySourceFileCreatedOnError() { @@ -4387,6 +4405,38 @@ await TestHelper.AssertExpectedExceptionAsync( e => Assert.AreEqual("CannotVerifyCopySource", e.ErrorCode)); } + [RecordedTest] + [ServiceVersion(Min = ShareClientOptions.ServiceVersion.V2024_08_04)] + public async Task UploadRangeFromUriAsync_SourceErrorAndStatusCode() + { + // Arrange + await using DisposingShare test = await GetTestShareAsync(shareName: GetNewShareName()); + ShareClient share = test.Share; + + ShareDirectoryClient directory = InstrumentClient(share.GetDirectoryClient(GetNewDirectoryName())); + await directory.CreateIfNotExistsAsync(); + + ShareFileClient sourceFile = InstrumentClient(directory.GetFileClient(GetNewFileName())); + + ShareFileClient destFile = directory.GetFileClient(GetNewFileName()); + await destFile.CreateAsync(maxSize: Constants.KB); + + HttpRange range = new HttpRange(0, Constants.KB); + + // Act + await TestHelper.AssertExpectedExceptionAsync( + destFile.UploadRangeFromUriAsync( + sourceUri: destFile.Uri, + range: range, + sourceRange: range), + e => + { + Assert.IsTrue(e.Message.Contains("CopySourceStatusCode: 401")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorCode: NoAuthenticationInformation")); + Assert.IsTrue(e.Message.Contains("CopySourceErrorMessage: Server failed to authenticate the request. Please refer to the information in the www-authenticate header.")); + }); + } + [RecordedTest] [ServiceVersion(Min = ShareClientOptions.ServiceVersion.V2021_06_08)] [TestCase(null)]