From 74e5e5d71e1c3f3633a2942acbd9d7a1d82549c6 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Sat, 7 Dec 2024 20:07:49 +0100
Subject: [PATCH 01/11] feat: add BodyInspectionHandler
Closes #482
---
.../Middleware/BodyInspectionHandler.cs | 95 +++++++++++++++
.../Options/BodyInspectionHandlerOption.cs | 44 +++++++
.../Middleware/BodyInspectionHandlerTests.cs | 112 ++++++++++++++++++
3 files changed, 251 insertions(+)
create mode 100644 src/http/httpClient/Middleware/BodyInspectionHandler.cs
create mode 100644 src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs
create mode 100644 tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
diff --git a/src/http/httpClient/Middleware/BodyInspectionHandler.cs b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
new file mode 100644
index 00000000..9dc7c506
--- /dev/null
+++ b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
@@ -0,0 +1,95 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using System;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Kiota.Http.HttpClientLibrary.Extensions;
+using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
+
+namespace Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
+
+///
+/// The Body Inspection Handler allows the developer to inspect the body of the request and response.
+///
+public class BodyInspectionHandler : DelegatingHandler
+{
+ private readonly BodyInspectionHandlerOption _defaultOptions;
+
+ ///
+ /// Create a new instance of
+ ///
+ /// Default options to apply to the handler
+ public BodyInspectionHandler(BodyInspectionHandlerOption? defaultOptions = null)
+ {
+ _defaultOptions = defaultOptions ?? new BodyInspectionHandlerOption();
+ }
+
+ ///
+ protected override async Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken
+ )
+ {
+ if (request == null)
+ throw new ArgumentNullException(nameof(request));
+
+ var options = request.GetRequestOption() ?? _defaultOptions;
+
+ Activity? activity;
+ if (request.GetRequestOption() is { } obsOptions)
+ {
+ var activitySource = ActivitySourceRegistry.DefaultInstance.GetOrCreateActivitySource(
+ obsOptions.TracerInstrumentationName
+ );
+ activity = activitySource?.StartActivity(
+ $"{nameof(RedirectHandler)}_{nameof(SendAsync)}"
+ );
+ activity?.SetTag("com.microsoft.kiota.handler.bodyInspection.enable", true);
+ }
+ else
+ {
+ activity = null;
+ }
+ try
+ {
+ if (options.InspectRequestBody)
+ {
+ options.RequestBody = await CopyToStreamAsync(request.Content);
+ }
+ var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ if (options.InspectResponseBody)
+ {
+ options.ResponseBody = await CopyToStreamAsync(response.Content);
+ }
+
+ return response;
+ }
+ finally
+ {
+ activity?.Dispose();
+ }
+
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER
+ [return: NotNullIfNotNull(nameof(httpContent))]
+#endif
+ static async Task CopyToStreamAsync(HttpContent? httpContent)
+ {
+ if (httpContent == null)
+ {
+ return null;
+ }
+
+ var stream = new MemoryStream();
+ await httpContent.CopyToAsync(stream);
+ stream.Position = 0;
+
+ return stream;
+ }
+ }
+}
diff --git a/src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs b/src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs
new file mode 100644
index 00000000..3cc8181b
--- /dev/null
+++ b/src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs
@@ -0,0 +1,44 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using System.IO;
+using Microsoft.Kiota.Abstractions;
+
+namespace Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
+
+///
+/// The Body Inspection Option allows the developer to inspect the body of the request and response.
+///
+public class BodyInspectionHandlerOption : IRequestOption
+{
+ ///
+ /// Gets or sets a value indicating whether the request body should be inspected.
+ /// Note tht this setting increases memory usae as the request body is copied to a new stream.
+ ///
+ public bool InspectRequestBody { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the response body should be inspected.
+ /// Note tht this setting increases memory usae as the request body is copied to a new stream.
+ ///
+ public bool InspectResponseBody { get; set; }
+
+ ///
+ /// Gets the request body stream for the current request. This stream is available
+ /// only if InspectRequestBody is set to true and the request contains a body.
+ /// This stream is not disposed of by kiota, you need to take care of that.
+ /// Note that this stream is a copy of the original request body stream, which has
+ /// impact on memory usage. Use adequately.
+ ///
+ public Stream? RequestBody { get; internal set; }
+
+ ///
+ /// Gets the response body stream for the current request. This stream is available
+ /// only if InspectResponseBody is set to true.
+ /// This stream is not disposed of by kiota, you need to take care of that.
+ /// Note that this stream is a copy of the original request body stream, which has
+ /// impact on memory usage. Use adequately.
+ ///
+ public Stream? ResponseBody { get; internal set; }
+}
diff --git a/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs b/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
new file mode 100644
index 00000000..a4eba432
--- /dev/null
+++ b/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
@@ -0,0 +1,112 @@
+using System.Net.Http;
+using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
+using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
+using Microsoft.Kiota.Http.HttpClientLibrary.Tests.Mocks;
+using Xunit;
+
+namespace Microsoft.Kiota.Http.HttpClientLibrary.Tests.Middleware;
+
+public class BodyInspectionHandlerTests : IDisposable
+{
+ private readonly List _disposables = [];
+
+ [Fact]
+ public void BodyInspectionHandlerConstruction()
+ {
+ using var defaultValue = new BodyInspectionHandler();
+ Assert.NotNull(defaultValue);
+ }
+
+ [Fact]
+ public async Task BodyInspectionHandlerGetsRequestBodyStream()
+ {
+ var option = new BodyInspectionHandlerOption { InspectRequestBody = true, };
+ using var invoker = GetMessageInvoker(new HttpResponseMessage(), option);
+
+ // When
+ var request = new HttpRequestMessage(HttpMethod.Post, "https://localhost")
+ {
+ Content = new StringContent("request test")
+ };
+ var response = await invoker.SendAsync(request, default);
+
+ // Then
+ Assert.NotNull(option.RequestBody);
+ Assert.Equal("request test", GetStringFromStream(option.RequestBody!));
+ }
+
+ [Fact]
+ public async Task BodyInspectionHandlerGetsNullRequestBodyStreamWhenThereIsNoRequestBody()
+ {
+ var option = new BodyInspectionHandlerOption { InspectRequestBody = true, };
+ using var invoker = GetMessageInvoker(new HttpResponseMessage(), option);
+
+ // When
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost");
+ var response = await invoker.SendAsync(request, default);
+
+ // Then
+ Assert.Null(option.RequestBody);
+ }
+
+ [Fact]
+ public async Task BodyInspectionHandlerGetsResponseBodyStream()
+ {
+ var option = new BodyInspectionHandlerOption { InspectResponseBody = true, };
+ using var invoker = GetMessageInvoker(CreateHttpResponseWithBody(), option);
+
+ // When
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost");
+ var response = await invoker.SendAsync(request, default);
+
+ // Then
+ Assert.NotNull(option.ResponseBody);
+ Assert.Equal("response test", GetStringFromStream(option.ResponseBody!));
+ Assert.Equal("response test", await response.Content.ReadAsStringAsync()); // response from option is separate from "normal" response stream
+ }
+
+ [Fact]
+ public async Task BodyInspectionHandlerGetsEmptyResponseBodyStreamWhenThereIsNoResponseBody()
+ {
+ var option = new BodyInspectionHandlerOption { InspectResponseBody = true, };
+ using var invoker = GetMessageInvoker(new HttpResponseMessage(), option);
+
+ // When
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost");
+ var response = await invoker.SendAsync(request, default);
+
+ // Then
+ Assert.NotNull(option.ResponseBody);
+ Assert.Equal(string.Empty, GetStringFromStream(option.ResponseBody!));
+ }
+
+ private static HttpResponseMessage CreateHttpResponseWithBody() =>
+ new() { Content = new StringContent("response test") };
+
+ private HttpMessageInvoker GetMessageInvoker(
+ HttpResponseMessage httpResponseMessage,
+ BodyInspectionHandlerOption option
+ )
+ {
+ var messageHandler = new MockRedirectHandler();
+ _disposables.Add(messageHandler);
+ _disposables.Add(httpResponseMessage);
+ messageHandler.SetHttpResponse(httpResponseMessage);
+ // Given
+ var handler = new BodyInspectionHandler(option) { InnerHandler = messageHandler };
+ _disposables.Add(handler);
+ return new HttpMessageInvoker(handler);
+ }
+
+ private static string GetStringFromStream(Stream stream)
+ {
+ using var reader = new StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+
+ public void Dispose()
+ {
+ _disposables.ForEach(static x => x.Dispose());
+ GC.SuppressFinalize(this);
+ }
+}
From 3c5023a1ffcc1bd8d1bbb9d5a9b36e5c5b262830 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Sat, 7 Dec 2024 21:41:06 +0100
Subject: [PATCH 02/11] Fix tracing names
---
src/http/httpClient/Middleware/BodyInspectionHandler.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/http/httpClient/Middleware/BodyInspectionHandler.cs b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
index 9dc7c506..3b6656a1 100644
--- a/src/http/httpClient/Middleware/BodyInspectionHandler.cs
+++ b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
@@ -48,7 +48,7 @@ CancellationToken cancellationToken
obsOptions.TracerInstrumentationName
);
activity = activitySource?.StartActivity(
- $"{nameof(RedirectHandler)}_{nameof(SendAsync)}"
+ $"{nameof(BodyInspectionHandler)}_{nameof(SendAsync)}"
);
activity?.SetTag("com.microsoft.kiota.handler.bodyInspection.enable", true);
}
From 3a9473e4c3c3cd0ff133d822ceccd7c1a715bc84 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Mon, 9 Dec 2024 18:11:24 +0100
Subject: [PATCH 03/11] Register in default list
---
src/http/httpClient/KiotaClientFactory.cs | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/http/httpClient/KiotaClientFactory.cs b/src/http/httpClient/KiotaClientFactory.cs
index 2f647c5c..d2953bb1 100644
--- a/src/http/httpClient/KiotaClientFactory.cs
+++ b/src/http/httpClient/KiotaClientFactory.cs
@@ -89,6 +89,7 @@ public static IList CreateDefaultHandlers(IRequestOption[]? o
ParametersNameDecodingOption? parametersNameDecodingOption = null;
UserAgentHandlerOption? userAgentHandlerOption = null;
HeadersInspectionHandlerOption? headersInspectionHandlerOption = null;
+ BodyInspectionHandlerOption? bodyInspectionHandlerOption = null;
foreach(var option in optionsForHandlers)
{
@@ -102,8 +103,10 @@ public static IList CreateDefaultHandlers(IRequestOption[]? o
parametersNameDecodingOption = parametersOption;
else if(userAgentHandlerOption == null && option is UserAgentHandlerOption userAgentOption)
userAgentHandlerOption = userAgentOption;
- else if(headersInspectionHandlerOption == null && option is HeadersInspectionHandlerOption headersOption)
- headersInspectionHandlerOption = headersOption;
+ else if(headersInspectionHandlerOption == null && option is HeadersInspectionHandlerOption headersInspectionOption)
+ headersInspectionHandlerOption = headersInspectionOption;
+ else if (bodyInspectionHandlerOption == null && option is BodyInspectionHandlerOption bodyInspectionOption)
+ bodyInspectionHandlerOption = bodyInspectionOption;
}
return new List
@@ -114,6 +117,7 @@ public static IList CreateDefaultHandlers(IRequestOption[]? o
parametersNameDecodingOption != null ? new ParametersNameDecodingHandler(parametersNameDecodingOption) : new ParametersNameDecodingHandler(),
userAgentHandlerOption != null ? new UserAgentHandler(userAgentHandlerOption) : new UserAgentHandler(),
headersInspectionHandlerOption != null ? new HeadersInspectionHandler(headersInspectionHandlerOption) : new HeadersInspectionHandler(),
+ bodyInspectionHandlerOption != null ? new BodyInspectionHandler(bodyInspectionHandlerOption) : new BodyInspectionHandler(),
};
}
@@ -132,6 +136,7 @@ public static IList CreateDefaultHandlers(IRequestOption[]? o
typeof(ParametersNameDecodingHandler),
typeof(UserAgentHandler),
typeof(HeadersInspectionHandler),
+ typeof(BodyInspectionHandler),
};
}
From 752845c03c61cf8f61a235b31715026053274a75 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Mon, 9 Dec 2024 18:17:31 +0100
Subject: [PATCH 04/11] Add test to make sure request is still valid
---
tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs b/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
index a4eba432..6341d1bf 100644
--- a/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
+++ b/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
@@ -33,6 +33,7 @@ public async Task BodyInspectionHandlerGetsRequestBodyStream()
// Then
Assert.NotNull(option.RequestBody);
Assert.Equal("request test", GetStringFromStream(option.RequestBody!));
+ Assert.Equal("request test", await request.Content.ReadAsStringAsync()); // response from option is separate from "normal" request stream
}
[Fact]
From 4fd91fc94804a16cf792f855e87e2f89ceea4a98 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Mon, 9 Dec 2024 18:17:31 +0100
Subject: [PATCH 05/11] Skip copy when non-seekable stream
---
.../httpClient/Middleware/BodyInspectionHandler.cs | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/http/httpClient/Middleware/BodyInspectionHandler.cs b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
index 3b6656a1..03f50042 100644
--- a/src/http/httpClient/Middleware/BodyInspectionHandler.cs
+++ b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
@@ -85,9 +85,18 @@ CancellationToken cancellationToken
return null;
}
+ if (httpContent.Headers.ContentLength == 0)
+ {
+ return Stream.Null;
+ }
+
var stream = new MemoryStream();
await httpContent.CopyToAsync(stream);
- stream.Position = 0;
+
+ if (stream.CanSeek)
+ {
+ stream.Position = 0;
+ }
return stream;
}
From 6813cd2cfdd4815bf501bc7a038852ad241e58b9 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Mon, 9 Dec 2024 18:45:05 +0100
Subject: [PATCH 06/11] dotnet format
---
src/http/httpClient/KiotaClientFactory.cs | 2 +-
.../httpClient/Middleware/BodyInspectionHandler.cs | 14 +++++++-------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/http/httpClient/KiotaClientFactory.cs b/src/http/httpClient/KiotaClientFactory.cs
index d2953bb1..d7dee716 100644
--- a/src/http/httpClient/KiotaClientFactory.cs
+++ b/src/http/httpClient/KiotaClientFactory.cs
@@ -105,7 +105,7 @@ public static IList CreateDefaultHandlers(IRequestOption[]? o
userAgentHandlerOption = userAgentOption;
else if(headersInspectionHandlerOption == null && option is HeadersInspectionHandlerOption headersInspectionOption)
headersInspectionHandlerOption = headersInspectionOption;
- else if (bodyInspectionHandlerOption == null && option is BodyInspectionHandlerOption bodyInspectionOption)
+ else if(bodyInspectionHandlerOption == null && option is BodyInspectionHandlerOption bodyInspectionOption)
bodyInspectionHandlerOption = bodyInspectionOption;
}
diff --git a/src/http/httpClient/Middleware/BodyInspectionHandler.cs b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
index 03f50042..0c6733cb 100644
--- a/src/http/httpClient/Middleware/BodyInspectionHandler.cs
+++ b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
@@ -36,13 +36,13 @@ protected override async Task SendAsync(
CancellationToken cancellationToken
)
{
- if (request == null)
+ if(request == null)
throw new ArgumentNullException(nameof(request));
var options = request.GetRequestOption() ?? _defaultOptions;
Activity? activity;
- if (request.GetRequestOption() is { } obsOptions)
+ if(request.GetRequestOption() is { } obsOptions)
{
var activitySource = ActivitySourceRegistry.DefaultInstance.GetOrCreateActivitySource(
obsOptions.TracerInstrumentationName
@@ -58,12 +58,12 @@ CancellationToken cancellationToken
}
try
{
- if (options.InspectRequestBody)
+ if(options.InspectRequestBody)
{
options.RequestBody = await CopyToStreamAsync(request.Content);
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
- if (options.InspectResponseBody)
+ if(options.InspectResponseBody)
{
options.ResponseBody = await CopyToStreamAsync(response.Content);
}
@@ -80,12 +80,12 @@ CancellationToken cancellationToken
#endif
static async Task CopyToStreamAsync(HttpContent? httpContent)
{
- if (httpContent == null)
+ if(httpContent == null)
{
return null;
}
- if (httpContent.Headers.ContentLength == 0)
+ if(httpContent.Headers.ContentLength == 0)
{
return Stream.Null;
}
@@ -93,7 +93,7 @@ CancellationToken cancellationToken
var stream = new MemoryStream();
await httpContent.CopyToAsync(stream);
- if (stream.CanSeek)
+ if(stream.CanSeek)
{
stream.Position = 0;
}
From bdc61a68d59dd657ae3a41420a4ec85655365dd7 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Tue, 10 Dec 2024 15:07:08 +0100
Subject: [PATCH 07/11] configureAwait + cancellationToken
---
src/http/httpClient/Middleware/BodyInspectionHandler.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/http/httpClient/Middleware/BodyInspectionHandler.cs b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
index 0c6733cb..a5494875 100644
--- a/src/http/httpClient/Middleware/BodyInspectionHandler.cs
+++ b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
@@ -60,12 +60,12 @@ CancellationToken cancellationToken
{
if(options.InspectRequestBody)
{
- options.RequestBody = await CopyToStreamAsync(request.Content);
+ options.RequestBody = await CopyToStreamAsync(request.Content, cancellationToken).ConfigureAwait(false);
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
if(options.InspectResponseBody)
{
- options.ResponseBody = await CopyToStreamAsync(response.Content);
+ options.ResponseBody = await CopyToStreamAsync(response.Content, cancellationToken).ConfigureAwait(false);
}
return response;
@@ -78,7 +78,7 @@ CancellationToken cancellationToken
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER
[return: NotNullIfNotNull(nameof(httpContent))]
#endif
- static async Task CopyToStreamAsync(HttpContent? httpContent)
+ static async Task CopyToStreamAsync(HttpContent? httpContent, CancellationToken cancellationToken)
{
if(httpContent == null)
{
@@ -91,7 +91,7 @@ CancellationToken cancellationToken
}
var stream = new MemoryStream();
- await httpContent.CopyToAsync(stream);
+ await httpContent.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
if(stream.CanSeek)
{
From 3fe5628c9041a87927af495f79e218fb6958a12f Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Tue, 10 Dec 2024 16:41:50 +0100
Subject: [PATCH 08/11] Fix cancellation token
---
src/http/httpClient/Middleware/BodyInspectionHandler.cs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/http/httpClient/Middleware/BodyInspectionHandler.cs b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
index a5494875..6a8591ba 100644
--- a/src/http/httpClient/Middleware/BodyInspectionHandler.cs
+++ b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
@@ -91,7 +91,13 @@ CancellationToken cancellationToken
}
var stream = new MemoryStream();
+
+#if NET5_0_OR_GREATER
await httpContent.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
+#else
+ await httpContent.CopyToAsync(stream).ConfigureAwait(false);
+#endif
+
if(stream.CanSeek)
{
From 5ed13f77b7c1cb94cb9ab794ce92a0085f74a588 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Tue, 10 Dec 2024 16:41:50 +0100
Subject: [PATCH 09/11] Test octet request stream
---
.../Middleware/BodyInspectionHandlerTests.cs | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs b/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
index 6341d1bf..093fa3db 100644
--- a/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
+++ b/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
@@ -36,6 +36,35 @@ public async Task BodyInspectionHandlerGetsRequestBodyStream()
Assert.Equal("request test", await request.Content.ReadAsStringAsync()); // response from option is separate from "normal" request stream
}
+ [Fact]
+ public async Task BodyInspectionHandlerGetsRequestBodyStreamWhenRequestIsOctetStream()
+ {
+ var option = new BodyInspectionHandlerOption { InspectRequestBody = true, };
+ using var invoker = GetMessageInvoker(new HttpResponseMessage(), option);
+
+ // When
+ var memoryStream = new MemoryStream();
+ var writer = new StreamWriter(memoryStream);
+ await writer.WriteAsync("request test");
+ await writer.FlushAsync();
+ memoryStream.Seek(0, SeekOrigin.Begin);
+
+ var request = new HttpRequestMessage(HttpMethod.Post, "https://localhost")
+ {
+ Content = new StreamContent(memoryStream)
+ };
+ request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(
+ "application/octet-stream"
+ );
+
+ var response = await invoker.SendAsync(request, default);
+
+ // Then
+ Assert.NotNull(option.RequestBody);
+ Assert.Equal("request test", GetStringFromStream(option.RequestBody!));
+ Assert.Equal("request test", await request.Content.ReadAsStringAsync()); // response from option is separate from "normal" request stream
+ }
+
[Fact]
public async Task BodyInspectionHandlerGetsNullRequestBodyStreamWhenThereIsNoRequestBody()
{
From 6c67482da3c979cf80a66be18fdf0f4355d47395 Mon Sep 17 00:00:00 2001
From: Marcin Jahn <10273406+marcinjahn@users.noreply.github.com>
Date: Thu, 12 Dec 2024 20:49:30 +0100
Subject: [PATCH 10/11] Use Stream.Null instead of null and fix net462 issues
---
.../Middleware/BodyInspectionHandler.cs | 19 +++++++++----------
.../Options/BodyInspectionHandlerOption.cs | 12 ++++++------
.../Middleware/BodyInspectionHandlerTests.cs | 10 +++-------
3 files changed, 18 insertions(+), 23 deletions(-)
diff --git a/src/http/httpClient/Middleware/BodyInspectionHandler.cs b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
index 6a8591ba..59b193c5 100644
--- a/src/http/httpClient/Middleware/BodyInspectionHandler.cs
+++ b/src/http/httpClient/Middleware/BodyInspectionHandler.cs
@@ -60,12 +60,14 @@ CancellationToken cancellationToken
{
if(options.InspectRequestBody)
{
- options.RequestBody = await CopyToStreamAsync(request.Content, cancellationToken).ConfigureAwait(false);
+ options.RequestBody = await CopyToStreamAsync(request.Content, cancellationToken)
+ .ConfigureAwait(false);
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
if(options.InspectResponseBody)
{
- options.ResponseBody = await CopyToStreamAsync(response.Content, cancellationToken).ConfigureAwait(false);
+ options.ResponseBody = await CopyToStreamAsync(response.Content, cancellationToken)
+ .ConfigureAwait(false);
}
return response;
@@ -78,14 +80,12 @@ CancellationToken cancellationToken
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER
[return: NotNullIfNotNull(nameof(httpContent))]
#endif
- static async Task CopyToStreamAsync(HttpContent? httpContent, CancellationToken cancellationToken)
+ static async Task CopyToStreamAsync(
+ HttpContent? httpContent,
+ CancellationToken cancellationToken
+ )
{
- if(httpContent == null)
- {
- return null;
- }
-
- if(httpContent.Headers.ContentLength == 0)
+ if(httpContent is null or { Headers.ContentLength: 0 })
{
return Stream.Null;
}
@@ -98,7 +98,6 @@ CancellationToken cancellationToken
await httpContent.CopyToAsync(stream).ConfigureAwait(false);
#endif
-
if(stream.CanSeek)
{
stream.Position = 0;
diff --git a/src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs b/src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs
index 3cc8181b..0afe71eb 100644
--- a/src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs
+++ b/src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs
@@ -26,19 +26,19 @@ public class BodyInspectionHandlerOption : IRequestOption
///
/// Gets the request body stream for the current request. This stream is available
- /// only if InspectRequestBody is set to true and the request contains a body.
- /// This stream is not disposed of by kiota, you need to take care of that.
+ /// only if InspectRequestBody is set to true and the request contains a body. Otherwise,
+ /// it's just Stream.Null. This stream is not disposed of by kiota, you need to take care of that.
/// Note that this stream is a copy of the original request body stream, which has
/// impact on memory usage. Use adequately.
///
- public Stream? RequestBody { get; internal set; }
+ public Stream RequestBody { get; internal set; } = Stream.Null;
///
/// Gets the response body stream for the current request. This stream is available
- /// only if InspectResponseBody is set to true.
- /// This stream is not disposed of by kiota, you need to take care of that.
+ /// only if InspectResponseBody is set to true and the response contains a body. Otherwise,
+ /// it's just Stream.Null. This stream is not disposed of by kiota, you need to take care of that.
/// Note that this stream is a copy of the original request body stream, which has
/// impact on memory usage. Use adequately.
///
- public Stream? ResponseBody { get; internal set; }
+ public Stream ResponseBody { get; internal set; } = Stream.Null;
}
diff --git a/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs b/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
index 093fa3db..176d8a6e 100644
--- a/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
+++ b/tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
@@ -31,7 +31,6 @@ public async Task BodyInspectionHandlerGetsRequestBodyStream()
var response = await invoker.SendAsync(request, default);
// Then
- Assert.NotNull(option.RequestBody);
Assert.Equal("request test", GetStringFromStream(option.RequestBody!));
Assert.Equal("request test", await request.Content.ReadAsStringAsync()); // response from option is separate from "normal" request stream
}
@@ -60,7 +59,6 @@ public async Task BodyInspectionHandlerGetsRequestBodyStreamWhenRequestIsOctetSt
var response = await invoker.SendAsync(request, default);
// Then
- Assert.NotNull(option.RequestBody);
Assert.Equal("request test", GetStringFromStream(option.RequestBody!));
Assert.Equal("request test", await request.Content.ReadAsStringAsync()); // response from option is separate from "normal" request stream
}
@@ -76,7 +74,7 @@ public async Task BodyInspectionHandlerGetsNullRequestBodyStreamWhenThereIsNoReq
var response = await invoker.SendAsync(request, default);
// Then
- Assert.Null(option.RequestBody);
+ Assert.Same(Stream.Null, option.RequestBody);
}
[Fact]
@@ -90,13 +88,12 @@ public async Task BodyInspectionHandlerGetsResponseBodyStream()
var response = await invoker.SendAsync(request, default);
// Then
- Assert.NotNull(option.ResponseBody);
Assert.Equal("response test", GetStringFromStream(option.ResponseBody!));
Assert.Equal("response test", await response.Content.ReadAsStringAsync()); // response from option is separate from "normal" response stream
}
[Fact]
- public async Task BodyInspectionHandlerGetsEmptyResponseBodyStreamWhenThereIsNoResponseBody()
+ public async Task BodyInspectionHandlerGetsNullResponseBodyStreamWhenThereIsNoResponseBody()
{
var option = new BodyInspectionHandlerOption { InspectResponseBody = true, };
using var invoker = GetMessageInvoker(new HttpResponseMessage(), option);
@@ -106,8 +103,7 @@ public async Task BodyInspectionHandlerGetsEmptyResponseBodyStreamWhenThereIsNoR
var response = await invoker.SendAsync(request, default);
// Then
- Assert.NotNull(option.ResponseBody);
- Assert.Equal(string.Empty, GetStringFromStream(option.ResponseBody!));
+ Assert.Same(Stream.Null, option.ResponseBody);
}
private static HttpResponseMessage CreateHttpResponseWithBody() =>
From 9eb310ffd1bf8dbf12a7744cf69d444529401663 Mon Sep 17 00:00:00 2001
From: Andrew Omondi
Date: Fri, 13 Dec 2024 11:51:11 +0300
Subject: [PATCH 11/11] bump version and release notes
---
CHANGELOG.md | 6 ++++++
Directory.Build.props | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 923c9fc7..495ff05e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.16.0] - 2024-12-13
+
+### Added
+
+- Added body inspection handler to enable inspection of request and response bodies. [#482](https://github.com/microsoft/kiota-dotnet/issues/482)
+
## [1.15.2] - 2024-11-13
### Changed
diff --git a/Directory.Build.props b/Directory.Build.props
index 400480e6..dd8db0b4 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- 1.15.2
+ 1.16.0
false