Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw IotHubClientException for file uploading with invalid correlation id #3159

Merged
merged 13 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions e2e/test/iothub/device/FileUploadE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpStatusCode, Func<HttpResponseMessage, Task<Exception>>> GetDefaultErrorMapping()
{
Expand All @@ -22,17 +24,16 @@ internal static IDictionary<HttpStatusCode, Func<HttpResponseMessage, Task<Excep
CreateMessageWhenDeviceNotFound(await GetExceptionMessageAsync(response).ConfigureAwait(false)),
IotHubClientErrorCode.DeviceNotFound)
},
{
{
HttpStatusCode.NotFound,
async (response) =>
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,
Expand Down Expand Up @@ -92,9 +93,63 @@ internal static Task<string> GetExceptionMessageAsync(HttpResponseMessage respon
return response.Content.ReadAsStringAsync();
}

internal static async Task<Tuple<string, IotHubClientErrorCode>> GetErrorCodeAndTrackingIdAsync(HttpResponseMessage response)
{
string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
ErrorPayload responseMessage = null;

try
{
IotHubExceptionResult result = JsonConvert.DeserializeObject<IotHubExceptionResult>(responseBody);
responseMessage = JsonConvert.DeserializeObject<ErrorPayload>(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<IotHubClientException> GenerateIotHubClientExceptionAsync(HttpResponseMessage response)
{
string message = await GetExceptionMessageAsync(response).ConfigureAwait(false);
Tuple<string, IotHubClientErrorCode> pair = await GetErrorCodeAndTrackingIdAsync(response).ConfigureAwait(false);

return new IotHubClientException(message, pair.Item2)
{
TrackingId = pair.Item1,
};
}
}
}
25 changes: 25 additions & 0 deletions iothub/device/src/Exceptions/ErrorPayload.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A class used as a model to deserialize one schema type of errors received from IoT hub.
/// </summary>
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; }
}
}
19 changes: 19 additions & 0 deletions iothub/device/src/IotHubExceptionResult.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A class used as a model to deserialize error response object received from IoT hub.
/// </summary>
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; }
}
}
2 changes: 1 addition & 1 deletion iothub/device/src/Transport/Http/HttpClientHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ private static async Task<Exception> MapToExceptionAsync(
{
if (!errorMapping.TryGetValue(response.StatusCode, out Func<HttpResponseMessage, Task<Exception>> func))
{
return new IotHubClientException(await ExceptionHandlingHelper.GetExceptionMessageAsync(response).ConfigureAwait(false))
return new IotHubClientException(await ClientExceptionHandlingHelper.GetExceptionMessageAsync(response).ConfigureAwait(false))
{
IsTransient = true,
};
Expand Down
2 changes: 1 addition & 1 deletion iothub/device/src/Transport/Http/HttpTransportHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal HttpTransportHandler(
httpsEndpoint,
context.IotHubConnectionCredentials,
additionalClientInformation,
ExceptionHandlingHelper.GetDefaultErrorMapping(),
ClientExceptionHandlingHelper.GetDefaultErrorMapping(),
s_defaultOperationTimeout,
httpClientHandler,
transportSettings);
Expand Down
75 changes: 75 additions & 0 deletions iothub/device/tests/ClientExceptionHandlingHelperTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, IotHubClientErrorCode> 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<string, IotHubClientErrorCode> pair = await ClientExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(httpResponseMessage).ConfigureAwait(false);
string trackingId = pair.Item1;
IotHubClientErrorCode errorCode = pair.Item2;

// assert

trackingId.Should().BeEmpty();
errorCode.Should().Be(IotHubClientErrorCode.Unknown);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 2 additions & 2 deletions iothub/service/src/Http/HttpMessageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ internal static async Task ValidateHttpResponseStatusAsync(HttpStatusCode expect
{
if (expectedHttpStatusCode != responseMessage.StatusCode)
{
string errorMessage = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false);
Tuple<string, IotHubServiceErrorCode> pair = await ExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(responseMessage);
string errorMessage = await ServiceExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false);
Tuple<string, IotHubServiceErrorCode> pair = await ServiceExceptionHandlingHelper.GetErrorCodeAndTrackingIdAsync(responseMessage).ConfigureAwait(false);
string trackingId = pair.Item1;
IotHubServiceErrorCode errorCode = pair.Item2;

Expand Down
Loading