diff --git a/e2e/test/iothub/device/FileUploadE2ETests.cs b/e2e/test/iothub/device/FileUploadE2ETests.cs index 2489ba7c23..2c468f078c 100644 --- a/e2e/test/iothub/device/FileUploadE2ETests.cs +++ b/e2e/test/iothub/device/FileUploadE2ETests.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using FluentAssertions; using Microsoft.Azure.Devices.Client; -using Microsoft.Azure.Devices.Client.Transport; using Microsoft.Azure.Devices.E2ETests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.WindowsAzure.Storage.Blob; @@ -146,19 +145,18 @@ private async Task UploadFileGranularAsync(Stream source, string filename, IotHu try { - // TODO: the HTTP layer handles errors differently and does not produce the right kind of exceptions. - // It should be updated to throw the same kind of exceptions as MQTT and AMQP. await deviceClient.CompleteFileUploadAsync(notification).ConfigureAwait(false); } - catch (IotHubClientException ex) + catch (IotHubClientException ex) when (ex.ErrorCode is IotHubClientErrorCode.ServerError) { // Gateway V1 flow - ex.ErrorCode.Should().Be(IotHubClientErrorCode.ServerError); } - catch (ArgumentException ex) + catch (IotHubClientException ex) when (ex.ErrorCode is IotHubClientErrorCode.IotHubFormatError) { // Gateway V2 flow - ex.Message.Should().Contain("400006"); + ex.Message.Should().Contain("Cannot decode correlation_id"); + ex.TrackingId.Should().NotBe(string.Empty); + ex.IsTransient.Should().BeFalse(); } } } diff --git a/iothub/device/src/Exceptions/ExceptionHandlingHelper.cs b/iothub/device/src/Exceptions/ClientExceptionHandlingHelper.cs similarity index 61% rename from iothub/device/src/Exceptions/ExceptionHandlingHelper.cs rename to iothub/device/src/Exceptions/ClientExceptionHandlingHelper.cs index cfa2576e36..6ae45f823e 100644 --- a/iothub/device/src/Exceptions/ExceptionHandlingHelper.cs +++ b/iothub/device/src/Exceptions/ClientExceptionHandlingHelper.cs @@ -3,13 +3,15 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Newtonsoft.Json; namespace Microsoft.Azure.Devices.Client { - internal sealed class ExceptionHandlingHelper + internal sealed class ClientExceptionHandlingHelper { internal static IDictionary>> GetDefaultErrorMapping() { @@ -22,17 +24,16 @@ internal static IDictionary new IotHubClientException( CreateMessageWhenDeviceNotFound(await GetExceptionMessageAsync(response).ConfigureAwait(false)), IotHubClientErrorCode.DeviceNotFound) }, - { + { HttpStatusCode.BadRequest, - async (response) => - new ArgumentException(await GetExceptionMessageAsync(response).ConfigureAwait(false)) + async (response) => await GenerateIotHubClientExceptionAsync(response).ConfigureAwait(false) }, { HttpStatusCode.Unauthorized, @@ -92,9 +93,63 @@ internal static Task GetExceptionMessageAsync(HttpResponseMessage respon return response.Content.ReadAsStringAsync(); } + internal static async Task> GetErrorCodeAndTrackingIdAsync(HttpResponseMessage response) + { + string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + ErrorPayload responseMessage = null; + + try + { + IotHubExceptionResult result = JsonConvert.DeserializeObject(responseBody); + responseMessage = JsonConvert.DeserializeObject(result.Message); + } + catch (JsonException ex) + { + if (Logging.IsEnabled) + Logging.Error( + nameof(GetErrorCodeAndTrackingIdAsync), + $"Failed to parse response content JSON: {ex.Message}. Message body: '{responseBody}.'"); + } + + if (responseMessage != null) + { + string trackingId = string.Empty; + if (responseMessage.TrackingId != null) + { + trackingId = responseMessage.TrackingId; + } + + if (responseMessage.ErrorCode != null) + { + if (int.TryParse(responseMessage.ErrorCode, NumberStyles.Any, CultureInfo.InvariantCulture, out int errorCodeInt)) + { + return Tuple.Create(trackingId, (IotHubClientErrorCode)errorCodeInt); + } + } + } + + if (Logging.IsEnabled) + Logging.Error( + nameof(GetErrorCodeAndTrackingIdAsync), + $"Failed to derive any error code from the response message: {responseBody}"); + + return Tuple.Create(string.Empty, IotHubClientErrorCode.Unknown); + } + private static string CreateMessageWhenDeviceNotFound(string deviceId) { return "Device {0} not registered".FormatInvariant(deviceId); } + + private static async Task GenerateIotHubClientExceptionAsync(HttpResponseMessage response) + { + string message = await GetExceptionMessageAsync(response).ConfigureAwait(false); + Tuple pair = await GetErrorCodeAndTrackingIdAsync(response).ConfigureAwait(false); + + return new IotHubClientException(message, pair.Item2) + { + TrackingId = pair.Item1, + }; + } } } diff --git a/iothub/device/src/Exceptions/ErrorPayload.cs b/iothub/device/src/Exceptions/ErrorPayload.cs new file mode 100644 index 0000000000..d3a09c63ef --- /dev/null +++ b/iothub/device/src/Exceptions/ErrorPayload.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; + +namespace Microsoft.Azure.Devices.Client +{ + /// + /// A class used as a model to deserialize one schema type of errors received from IoT hub. + /// + internal sealed class ErrorPayload + { + [JsonProperty("errorCode")] + internal string ErrorCode { get; set; } + + [JsonProperty("trackingId")] + internal string TrackingId { get; set; } + + [JsonProperty("message")] + internal string Message { get; set; } + + [JsonProperty("timestampUtc")] + internal string OccurredOnUtc { get; set; } + } +} diff --git a/iothub/device/src/IotHubExceptionResult.cs b/iothub/device/src/IotHubExceptionResult.cs new file mode 100644 index 0000000000..fa30b267ce --- /dev/null +++ b/iothub/device/src/IotHubExceptionResult.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Azure.Devices.Client +{ + /// + /// A class used as a model to deserialize error response object received from IoT hub. + /// + internal sealed class IotHubExceptionResult + { + [SuppressMessage("Usage", "CA1507: Use nameof in place of string literal 'Message'", + Justification = "This JsonProperty annotation depends on service-defined contract (name) and is independent of the property name selected by the SDK.")] + [JsonProperty("Message")] + internal string Message { get; set; } + } +} diff --git a/iothub/device/src/Transport/Http/HttpClientHelper.cs b/iothub/device/src/Transport/Http/HttpClientHelper.cs index 210e6742a0..608e66cfe2 100644 --- a/iothub/device/src/Transport/Http/HttpClientHelper.cs +++ b/iothub/device/src/Transport/Http/HttpClientHelper.cs @@ -215,7 +215,7 @@ private static async Task MapToExceptionAsync( { if (!errorMapping.TryGetValue(response.StatusCode, out Func> func)) { - return new IotHubClientException(await ExceptionHandlingHelper.GetExceptionMessageAsync(response).ConfigureAwait(false)) + return new IotHubClientException(await ClientExceptionHandlingHelper.GetExceptionMessageAsync(response).ConfigureAwait(false)) { IsTransient = true, }; diff --git a/iothub/device/src/Transport/Http/HttpTransportHandler.cs b/iothub/device/src/Transport/Http/HttpTransportHandler.cs index ef161cd922..f9b285c054 100644 --- a/iothub/device/src/Transport/Http/HttpTransportHandler.cs +++ b/iothub/device/src/Transport/Http/HttpTransportHandler.cs @@ -42,7 +42,7 @@ internal HttpTransportHandler( httpsEndpoint, context.IotHubConnectionCredentials, additionalClientInformation, - ExceptionHandlingHelper.GetDefaultErrorMapping(), + ClientExceptionHandlingHelper.GetDefaultErrorMapping(), s_defaultOperationTimeout, httpClientHandler, transportSettings); diff --git a/iothub/device/tests/ClientExceptionHandlingHelperTests.cs b/iothub/device/tests/ClientExceptionHandlingHelperTests.cs new file mode 100644 index 0000000000..6ff1e9451c --- /dev/null +++ b/iothub/device/tests/ClientExceptionHandlingHelperTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Net.Http; +using System.Net; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using FluentAssertions; +using System; +using System.Threading.Tasks; + +namespace Microsoft.Azure.Devices.Client.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class ClientExceptionHandlingHelperTests + { + [TestMethod] + public async Task GetErrorCodeAndTrackingIdAsync_MessagePayloadDoubleEscaped_ValidErrorCodeAndTrackingId() + { + // arrange + + const IotHubClientErrorCode expectedErrorCode = IotHubClientErrorCode.IotHubFormatError; + const string expectedTrackingId = "E8A1D62DF1FB4F2F908B2F1492620D6B-G2"; + + using var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest); + var exceptionResult = new IotHubExceptionResult + { + Message = JsonConvert.SerializeObject(new ErrorPayload + { + ErrorCode = ((int)expectedErrorCode).ToString(), + TrackingId = expectedTrackingId, + Message = "Cannot decode correlation_id", + OccurredOnUtc = "2023-03-14T16:57:54.324613222+00:00", + }), + }; + httpResponseMessage.Content = new StringContent(JsonConvert.SerializeObject(exceptionResult)); + + // act + + Tuple pair = await ClientExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false); + string trackingId = pair.Item1; + IotHubClientErrorCode errorCode = pair.Item2; + + // assert + + trackingId.Should().Be(expectedTrackingId); + errorCode.Should().Be(expectedErrorCode); + } + + [TestMethod] + public async Task GetErrorCodeAndTrackingIdAsync_NoContentErrorCode_UnknownErrorCodeAndEmptyTrackingId() + { + // arrange + + using var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest); + var exceptionResult = new IotHubExceptionResult + { + Message = "", + }; + httpResponseMessage.Content = new StringContent(JsonConvert.SerializeObject(exceptionResult)); + + // act + + Tuple pair = await ClientExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false); + string trackingId = pair.Item1; + IotHubClientErrorCode errorCode = pair.Item2; + + // assert + + trackingId.Should().BeEmpty(); + errorCode.Should().Be(IotHubClientErrorCode.Unknown); + } + } +} diff --git a/iothub/service/src/Exceptions/ExceptionHandlingHelper.cs b/iothub/service/src/Exceptions/ServiceExceptionHandlingHelper.cs similarity index 99% rename from iothub/service/src/Exceptions/ExceptionHandlingHelper.cs rename to iothub/service/src/Exceptions/ServiceExceptionHandlingHelper.cs index 94db82d171..96c2215ef6 100644 --- a/iothub/service/src/Exceptions/ExceptionHandlingHelper.cs +++ b/iothub/service/src/Exceptions/ServiceExceptionHandlingHelper.cs @@ -9,7 +9,7 @@ namespace Microsoft.Azure.Devices { - internal sealed class ExceptionHandlingHelper + internal sealed class ServiceExceptionHandlingHelper { private const string MessageFieldErrorCode = "errorCode"; private const string HttpErrorCodeName = "iothub-errorcode"; diff --git a/iothub/service/src/Http/HttpMessageHelper.cs b/iothub/service/src/Http/HttpMessageHelper.cs index 71ea3df0df..3491ad6f54 100644 --- a/iothub/service/src/Http/HttpMessageHelper.cs +++ b/iothub/service/src/Http/HttpMessageHelper.cs @@ -47,8 +47,8 @@ internal static async Task ValidateHttpResponseStatusAsync(HttpStatusCode expect { if (expectedHttpStatusCode != responseMessage.StatusCode) { - string errorMessage = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); - Tuple pair = await ExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(responseMessage); + string errorMessage = await ServiceExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); + Tuple pair = await ServiceExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(responseMessage).ConfigureAwait(false); string trackingId = pair.Item1; IotHubServiceErrorCode errorCode = pair.Item2; diff --git a/iothub/service/tests/Exceptions/ExceptionHandlingHelperTests.cs b/iothub/service/tests/Exceptions/ServiceExceptionHandlingHelperTests.cs similarity index 79% rename from iothub/service/tests/Exceptions/ExceptionHandlingHelperTests.cs rename to iothub/service/tests/Exceptions/ServiceExceptionHandlingHelperTests.cs index 9f32a604cd..734ef00bcf 100644 --- a/iothub/service/tests/Exceptions/ExceptionHandlingHelperTests.cs +++ b/iothub/service/tests/Exceptions/ServiceExceptionHandlingHelperTests.cs @@ -13,10 +13,11 @@ namespace Microsoft.Azure.Devices.Tests.Exceptions { [TestClass] [TestCategory("Unit")] - public class ExceptionHandlingHelperTests + public class ServiceExceptionHandlingHelperTests + { [TestMethod] - public async Task GetExceptionCodeAsync_NumericErrorCode_InResponseMessage_ValidErrorCode() + public async Task GetErrorCodeAndTrackingIdAsync_NumericErrorCode_InResponseMessage_ValidErrorCode() { // arrange using var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest); @@ -34,7 +35,7 @@ public async Task GetExceptionCodeAsync_NumericErrorCode_InResponseMessage_Valid httpResponseMessage.Content = new StringContent(JsonConvert.SerializeObject(exceptionResult)); // act - Tuple pair = await ExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage); + Tuple pair = await ServiceExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false); string trackingId = pair.Item1; IotHubServiceErrorCode errorCode = pair.Item2; @@ -44,7 +45,7 @@ public async Task GetExceptionCodeAsync_NumericErrorCode_InResponseMessage_Valid } [TestMethod] - public async Task GetExceptionCodeAsync_MessagePayloadDoubleEscaped() + public async Task GetErrorCodeAndTrackingIdAsync_MessagePayloadDoubleEscaped() { // arrange const string expectedTrackingId = "95ae23a6a159445681f6a52aebc99ab0-TimeStamp:10/19/2022 16:47:22"; @@ -64,7 +65,7 @@ public async Task GetExceptionCodeAsync_MessagePayloadDoubleEscaped() httpResponseMessage.Content = new StringContent(JsonConvert.SerializeObject(exceptionResult)); // act - Tuple pair = await ExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage); + Tuple pair = await ServiceExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false); string trackingId = pair.Item1; IotHubServiceErrorCode errorCode = pair.Item2; @@ -74,7 +75,7 @@ public async Task GetExceptionCodeAsync_MessagePayloadDoubleEscaped() } [TestMethod] - public async Task GetExceptionCodeAsync_StructuredBodyFormat2() + public async Task GetErrorCodeAndTrackingIdAsync_StructuredBodyFormat2() { // arrange const string expectedTrackingId = "aeec4c1e4e914a4c9f40fdba7be68fa5-G:0-TimeStamp:10/18/2022 20:50:39"; @@ -84,7 +85,7 @@ public async Task GetExceptionCodeAsync_StructuredBodyFormat2() httpResponseMessage.Content = new StringContent(exceptionResult); // act - Tuple result = await ExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage); + Tuple result = await ServiceExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false); // assert result.Item1.Should().Be(expectedTrackingId); @@ -92,7 +93,7 @@ public async Task GetExceptionCodeAsync_StructuredBodyFormat2() } [TestMethod] - public async Task GetExceptionCodeAsync_NonNumericErrorCode_InPlainString_ValidErrorCode() + public async Task GetErrorCodeAndTrackingIdAsync_NonNumericErrorCode_InPlainString_ValidErrorCode() { // arrange using var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest); @@ -104,7 +105,7 @@ public async Task GetExceptionCodeAsync_NonNumericErrorCode_InPlainString_ValidE httpResponseMessage.Content = new StringContent(JsonConvert.SerializeObject(exceptionResult)); // act - Tuple pair = await ExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage); + Tuple pair = await ServiceExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false); IotHubServiceErrorCode errorCode = pair.Item2; // assert @@ -112,7 +113,7 @@ public async Task GetExceptionCodeAsync_NonNumericErrorCode_InPlainString_ValidE } [TestMethod] - public async Task GetExceptionCodeAsync_InvalidContent_InPlainString_UnknownErrorCode() + public async Task GetErrorCodeAndTrackingIdAsync_InvalidContent_InPlainString_UnknownErrorCode() { // arrange using var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest); @@ -124,7 +125,7 @@ public async Task GetExceptionCodeAsync_InvalidContent_InPlainString_UnknownErro httpResponseMessage.Content = new StringContent(JsonConvert.SerializeObject(exceptionResult)); // act - Tuple pair = await ExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage); + Tuple pair = await ServiceExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false); IotHubServiceErrorCode errorCode = pair.Item2; // assert @@ -132,7 +133,7 @@ public async Task GetExceptionCodeAsync_InvalidContent_InPlainString_UnknownErro } [TestMethod] - public async Task GetExceptionCodeAsync_NoContentErrorCode_UnknownErrorCode() + public async Task GetErrorCodeAndTrackingIdAsync_NoContentErrorCode_UnknownErrorCode() { // arrange using var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest); @@ -143,7 +144,7 @@ public async Task GetExceptionCodeAsync_NoContentErrorCode_UnknownErrorCode() httpResponseMessage.Content = new StringContent(JsonConvert.SerializeObject(exceptionResult)); // act - Tuple pair = await ExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage); + Tuple pair = await ServiceExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false); IotHubServiceErrorCode errorCode = pair.Item2; // assert diff --git a/iothub/service/tests/Feedback/FeedbackBatchTests.cs b/iothub/service/tests/Feedback/FeedbackBatchTests.cs index d236de6208..04d0a78199 100644 --- a/iothub/service/tests/Feedback/FeedbackBatchTests.cs +++ b/iothub/service/tests/Feedback/FeedbackBatchTests.cs @@ -2,12 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using FluentAssertions; -using Microsoft.Azure.Amqp; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Azure.Devices.Tests.Feedback