Skip to content

Commit c78b39d

Browse files
author
Andrew Omondi
committed
fix: resolved handling of larger batch request message
1 parent 60d7c86 commit c78b39d

File tree

6 files changed

+169
-14
lines changed

6 files changed

+169
-14
lines changed

src/Microsoft.Graph.Core/Exceptions/ErrorConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ internal static class Messages
8383

8484
internal static string PageIteratorRequestError = "Error occured when making a request with the page iterator. See inner exception for more details.";
8585

86+
internal static string BatchRequestError = "Error occured when making the batch request. See inner exception for more details.";
87+
8688
public static string InvalidProxyArgument = "Proxy cannot be set more once. Proxy can only be set on the proxy or defaultHttpHandler argument and not both.";
8789
}
8890
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Microsoft.Kiota.Abstractions.Serialization;
2+
3+
namespace Microsoft.Graph;
4+
5+
/// <summary>
6+
/// Extension helpers for the <see cref="IParseNode"/>
7+
/// </summary>
8+
public static class ParseNodeExtensions
9+
{
10+
internal static string GetErrorMessage(this IParseNode responseParseNode)
11+
{
12+
var errorParseNode = responseParseNode.GetChildNode("error");
13+
// concatenate the error code and message
14+
return $"{errorParseNode?.GetChildNode("code")?.GetStringValue()} : {errorParseNode?.GetChildNode("message")?.GetStringValue()}";
15+
}
16+
}

src/Microsoft.Graph.Core/Requests/BatchRequestBuilder.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ namespace Microsoft.Graph.Core.Requests
77
using System;
88
using System.Collections.Generic;
99
using System.Net.Http;
10+
using System.Text.Json;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Microsoft.Kiota.Abstractions;
1314
using Microsoft.Kiota.Abstractions.Serialization;
15+
using Microsoft.Kiota.Serialization.Json;
1416

1517
/// <summary>
1618
/// The type BatchRequestBuilder
@@ -58,7 +60,9 @@ public async Task<BatchResponseContent> PostAsync(BatchRequestContent batchReque
5860
var nativeResponseHandler = new NativeResponseHandler();
5961
requestInfo.SetResponseHandler(nativeResponseHandler);
6062
await this.RequestAdapter.SendNoContentAsync(requestInfo, cancellationToken: cancellationToken);
61-
return new BatchResponseContent(nativeResponseHandler.Value as HttpResponseMessage, errorMappings);
63+
var httpResponseMessage = nativeResponseHandler.Value as HttpResponseMessage;
64+
await ThrowIfFailedResponseAsync(httpResponseMessage);
65+
return new BatchResponseContent(httpResponseMessage, errorMappings);
6266
}
6367

6468
/// <summary>
@@ -99,5 +103,26 @@ public async Task<RequestInformation> ToPostRequestInformationAsync(BatchRequest
99103
requestInfo.Headers.Add("Content-Type", CoreConstants.MimeTypeNames.Application.Json);
100104
return requestInfo;
101105
}
106+
107+
private static async Task ThrowIfFailedResponseAsync(HttpResponseMessage httpResponseMessage)
108+
{
109+
if (httpResponseMessage.IsSuccessStatusCode) return;
110+
111+
if (CoreConstants.MimeTypeNames.Application.Json.Equals(httpResponseMessage.Content?.Headers?.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase))
112+
{
113+
using var responseContent = await httpResponseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);
114+
using var document = await JsonDocument.ParseAsync(responseContent).ConfigureAwait(false);
115+
var parsable = new JsonParseNode(document.RootElement);
116+
throw new ServiceException(ErrorConstants.Messages.BatchRequestError, httpResponseMessage.Headers, (int)httpResponseMessage.StatusCode, new Exception(parsable.GetErrorMessage()));
117+
}
118+
119+
var responseStringContent = string.Empty;
120+
if (httpResponseMessage.Content != null)
121+
{
122+
responseStringContent = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
123+
}
124+
125+
throw new ServiceException(ErrorConstants.Messages.BatchRequestError, httpResponseMessage.Headers, (int)httpResponseMessage.StatusCode, responseStringContent);
126+
}
102127
}
103128
}

src/Microsoft.Graph.Core/Requests/Content/BatchResponseContent.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public class BatchResponseContent
3232
public BatchResponseContent(HttpResponseMessage httpResponseMessage, Dictionary<string, ParsableFactory<IParsable>> errorMappings = null)
3333
{
3434
this.batchResponseMessage = httpResponseMessage ?? throw new ArgumentNullException(nameof(httpResponseMessage));
35-
this.apiErrorMappings = errorMappings ?? new();
35+
this.apiErrorMappings = errorMappings ?? new Dictionary<string, ParsableFactory<IParsable>>(StringComparer.OrdinalIgnoreCase) {
36+
{"XXX", (parsable) => new ServiceException(ErrorConstants.Messages.BatchRequestError, new Exception(parsable.GetErrorMessage())) }
37+
};
3638
}
3739

3840
/// <summary>

src/Microsoft.Graph.Core/Tasks/PageIterator.cs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,7 @@ public static PageIterator<TEntity, TCollectionPage> CreatePageIterator(IRequest
115115
_processPageItemCallback = callback,
116116
_requestConfigurator = requestConfigurator,
117117
_errorMapping = errorMapping ?? new Dictionary<string, ParsableFactory<IParsable>>(StringComparer.OrdinalIgnoreCase) {
118-
{"4XX", (parsable) => new ServiceException(ErrorConstants.Messages.PageIteratorRequestError,new Exception(GetErrorMessageFromParsable(parsable))) },
119-
{"5XX", (parsable) => new ServiceException(ErrorConstants.Messages.PageIteratorRequestError,new Exception(GetErrorMessageFromParsable(parsable))) }
118+
{"XXX", (parsable) => new ServiceException(ErrorConstants.Messages.PageIteratorRequestError,new Exception(parsable.GetErrorMessage())) }
120119
},
121120
State = PagingState.NotStarted
122121
};
@@ -172,15 +171,14 @@ public static PageIterator<TEntity, TCollectionPage> CreatePageIterator(IRequest
172171
_asyncProcessPageItemCallback = asyncCallback,
173172
_requestConfigurator = requestConfigurator,
174173
_errorMapping = errorMapping ?? new Dictionary<string, ParsableFactory<IParsable>>(StringComparer.OrdinalIgnoreCase) {
175-
{"4XX", (parsable) => new ServiceException(ErrorConstants.Messages.PageIteratorRequestError,new Exception(GetErrorMessageFromParsable(parsable))) },
176-
{"5XX", (parsable) =>new ServiceException(ErrorConstants.Messages.PageIteratorRequestError,new Exception(GetErrorMessageFromParsable(parsable))) },
174+
{"XXX", (parsable) => new ServiceException(ErrorConstants.Messages.PageIteratorRequestError,new Exception(parsable.GetErrorMessage())) }
177175
},
178176
State = PagingState.NotStarted
179177
};
180178
}
181179

182180
/// <summary>
183-
/// Iterate across the content of a a single results page with the callback.
181+
/// Iterate across the content of a single results page with the callback.
184182
/// </summary>
185183
/// <returns>A boolean value that indicates whether the callback cancelled
186184
/// iterating across the page results or whether there are more pages to page.
@@ -393,13 +391,6 @@ private static string ExtractNextLinkFromParsable(TCollectionPage parsableCollec
393391
// the next link property may not be defined in the response schema so we also check its presence in the additional data bag
394392
return parsableCollection.AdditionalData.TryGetValue(CoreConstants.OdataInstanceAnnotations.NextLink, out var nextLink) ? nextLink.ToString() : string.Empty;
395393
}
396-
397-
private static string GetErrorMessageFromParsable(IParseNode responseParseNode)
398-
{
399-
var errorParseNode = responseParseNode.GetChildNode("error");
400-
// concatenate the error code and message
401-
return $"{errorParseNode?.GetChildNode("code")?.GetStringValue()} : {errorParseNode?.GetChildNode("message")?.GetStringValue()}";
402-
}
403394
}
404395

405396
/// <summary>

tests/Microsoft.Graph.DotnetCore.Core.Test/Requests/BatchRequestBuilderTests.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ namespace Microsoft.Graph.DotnetCore.Core.Test.Requests
66
{
77
using System.Collections.Generic;
88
using System.Net.Http;
9+
using System.Text;
10+
using System.Threading;
911
using System.Threading.Tasks;
1012
using Microsoft.Graph.Core.Requests;
1113
using Microsoft.Graph.DotnetCore.Core.Test.Mocks;
14+
using Microsoft.Kiota.Abstractions;
1215
using Microsoft.Kiota.Abstractions.Authentication;
16+
using Microsoft.Kiota.Abstractions.Serialization;
17+
using Moq;
1318
using Xunit;
1419

1520
public class BatchRequestBuilderTests
@@ -42,5 +47,119 @@ public async Task BatchRequestBuilderAsync()
4247
Assert.Equal("{+baseurl}/$batch", requestInformation.UrlTemplate);
4348
Assert.Equal(baseClient.RequestAdapter, batchRequestBuilder.RequestAdapter);
4449
}
50+
51+
52+
[Fact]
53+
public async Task BatchRequestBuilderPostAsyncHandlesDoesNotThrowExceptionAsync()
54+
{
55+
// Arrange
56+
var requestAdapter = new Mock<IRequestAdapter>();
57+
IBaseClient baseClient = new BaseClient(requestAdapter.Object);
58+
59+
var errorResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
60+
{
61+
Content = new StringContent("{}", Encoding.UTF8, "application/json"),//dummy content
62+
};
63+
requestAdapter
64+
.Setup(requestAdapter => requestAdapter.SendNoContentAsync(It.IsAny<RequestInformation>(), It.IsAny<Dictionary<string, ParsableFactory<IParsable>>>(), It.IsAny<CancellationToken>()))
65+
.Callback((RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>> errorMapping, CancellationToken cancellationToken) => ((NativeResponseHandler)requestInfo.GetRequestOption<ResponseHandlerOption>().ResponseHandler).Value = errorResponseMessage)
66+
.Returns(Task.FromResult(0));
67+
68+
// Act
69+
var batchRequestBuilder = new BatchRequestBuilder(baseClient.RequestAdapter);
70+
71+
// 4. Create batch request content to be sent out
72+
// 4.1 Create HttpRequestMessages for the content
73+
HttpRequestMessage httpRequestMessage1 = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me/");
74+
75+
// 4.2 Create batch request steps with request ids.
76+
BatchRequestStep requestStep1 = new BatchRequestStep("1", httpRequestMessage1);
77+
78+
// 4.3 Add batch request steps to BatchRequestContent.
79+
#pragma warning disable CS0618 // Type or member is obsolete use the BatchRequestContentCollection for making batch requests
80+
BatchRequestContent batchRequestContent = new BatchRequestContent(baseClient, requestStep1);
81+
#pragma warning restore CS0618 // Type or member is obsolete use the BatchRequestContentCollection for making batch requests
82+
var responseContent = await batchRequestBuilder.PostAsync(batchRequestContent);
83+
84+
// Assert
85+
Assert.NotNull(responseContent);
86+
}
87+
88+
[Fact]
89+
public async Task BatchRequestBuilderPostAsyncHandlesNonSuccessStatusWithJsonResponseAsync()
90+
{
91+
// Arrange
92+
var requestAdapter = new Mock<IRequestAdapter>();
93+
IBaseClient baseClient = new BaseClient(requestAdapter.Object);
94+
95+
var errorResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized)
96+
{
97+
Content = new StringContent("{\"error\": {\"code\": \"20117\",\"message\": \"An item with this name already exists in this location.\",\"innerError\":{\"request-id\": \"nothing1b13-45cd-new-92be873c5781\",\"date\": \"2019-03-22T23:17:50\"}}}", Encoding.UTF8, "application/json"),
98+
};
99+
requestAdapter
100+
.Setup(requestAdapter => requestAdapter.SendNoContentAsync(It.IsAny<RequestInformation>(), It.IsAny<Dictionary<string, ParsableFactory<IParsable>>>(), It.IsAny<CancellationToken>()))
101+
.Callback((RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>> errorMapping, CancellationToken cancellationToken) => ((NativeResponseHandler)requestInfo.GetRequestOption<ResponseHandlerOption>().ResponseHandler).Value = errorResponseMessage)
102+
.Returns(Task.FromResult(0));
103+
104+
// Act
105+
var batchRequestBuilder = new BatchRequestBuilder(baseClient.RequestAdapter);
106+
107+
// 4. Create batch request content to be sent out
108+
// 4.1 Create HttpRequestMessages for the content
109+
HttpRequestMessage httpRequestMessage1 = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me/");
110+
111+
// 4.2 Create batch request steps with request ids.
112+
BatchRequestStep requestStep1 = new BatchRequestStep("1", httpRequestMessage1);
113+
114+
// 4.3 Add batch request steps to BatchRequestContent.
115+
#pragma warning disable CS0618 // Type or member is obsolete use the BatchRequestContentCollection for making batch requests
116+
BatchRequestContent batchRequestContent = new BatchRequestContent(baseClient, requestStep1);
117+
#pragma warning restore CS0618 // Type or member is obsolete use the BatchRequestContentCollection for making batch requests
118+
var serviceException = await Assert.ThrowsAsync<ServiceException>(async () => await batchRequestBuilder.PostAsync(batchRequestContent));
119+
120+
// Assert
121+
Assert.Equal(ErrorConstants.Messages.BatchRequestError, serviceException.Message);
122+
Assert.Equal(401, serviceException.ResponseStatusCode);
123+
Assert.NotNull(serviceException.InnerException);
124+
Assert.Equal("20117 : An item with this name already exists in this location.", serviceException.InnerException.Message);
125+
}
126+
127+
[Fact]
128+
public async Task BatchRequestBuilderPostAsyncHandlesNonSuccessStatusWithNonJsonResponseAsync()
129+
{
130+
// Arrange
131+
var requestAdapter = new Mock<IRequestAdapter>();
132+
IBaseClient baseClient = new BaseClient(requestAdapter.Object);
133+
134+
var errorResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.Conflict)
135+
{
136+
Content = new StringContent("<html>This is random html</html>", Encoding.UTF8, "text/plain"),
137+
};
138+
requestAdapter
139+
.Setup(requestAdapter => requestAdapter.SendNoContentAsync(It.IsAny<RequestInformation>(), It.IsAny<Dictionary<string, ParsableFactory<IParsable>>>(), It.IsAny<CancellationToken>()))
140+
.Callback((RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>> errorMapping, CancellationToken cancellationToken) => ((NativeResponseHandler)requestInfo.GetRequestOption<ResponseHandlerOption>().ResponseHandler).Value = errorResponseMessage)
141+
.Returns(Task.FromResult(0));
142+
143+
// Act
144+
var batchRequestBuilder = new BatchRequestBuilder(baseClient.RequestAdapter);
145+
146+
// 4. Create batch request content to be sent out
147+
// 4.1 Create HttpRequestMessages for the content
148+
HttpRequestMessage httpRequestMessage1 = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me/");
149+
150+
// 4.2 Create batch request steps with request ids.
151+
BatchRequestStep requestStep1 = new BatchRequestStep("1", httpRequestMessage1);
152+
153+
// 4.3 Add batch request steps to BatchRequestContent.
154+
#pragma warning disable CS0618 // Type or member is obsolete use the BatchRequestContentCollection for making batch requests
155+
BatchRequestContent batchRequestContent = new BatchRequestContent(baseClient, requestStep1);
156+
#pragma warning restore CS0618 // Type or member is obsolete use the BatchRequestContentCollection for making batch requests
157+
var serviceException = await Assert.ThrowsAsync<ServiceException>(async () => await batchRequestBuilder.PostAsync(batchRequestContent));
158+
159+
// Assert
160+
Assert.Equal(ErrorConstants.Messages.BatchRequestError, serviceException.Message);
161+
Assert.Equal(409, serviceException.ResponseStatusCode);
162+
Assert.Equal("<html>This is random html</html>", serviceException.RawResponseBody);
163+
}
45164
}
46165
}

0 commit comments

Comments
 (0)