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