forked from microsoft/kiota-dotnet
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
3 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// The Body Inspection Handler allows the developer to inspect the body of the request and response. | ||
/// </summary> | ||
public class BodyInspectionHandler : DelegatingHandler | ||
{ | ||
private readonly BodyInspectionHandlerOption _defaultOptions; | ||
|
||
/// <summary> | ||
/// Create a new instance of <see cref="BodyInspectionHandler"/> | ||
/// </summary> | ||
/// <param name="defaultOptions">Default options to apply to the handler</param> | ||
public BodyInspectionHandler(BodyInspectionHandlerOption? defaultOptions = null) | ||
{ | ||
_defaultOptions = defaultOptions ?? new BodyInspectionHandlerOption(); | ||
} | ||
|
||
/// <inheritdoc/> | ||
protected override async Task<HttpResponseMessage> SendAsync( | ||
HttpRequestMessage request, | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
if (request == null) | ||
throw new ArgumentNullException(nameof(request)); | ||
|
||
var options = request.GetRequestOption<BodyInspectionHandlerOption>() ?? _defaultOptions; | ||
|
||
Activity? activity; | ||
if (request.GetRequestOption<ObservabilityOptions>() 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<Stream?> CopyToStreamAsync(HttpContent? httpContent) | ||
{ | ||
if (httpContent == null) | ||
{ | ||
return null; | ||
} | ||
|
||
var stream = new MemoryStream(); | ||
await httpContent.CopyToAsync(stream); | ||
stream.Position = 0; | ||
|
||
return stream; | ||
} | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
src/http/httpClient/Middleware/Options/BodyInspectionHandlerOption.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// The Body Inspection Option allows the developer to inspect the body of the request and response. | ||
/// </summary> | ||
public class BodyInspectionHandlerOption : IRequestOption | ||
{ | ||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
public bool InspectRequestBody { get; set; } | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
public bool InspectResponseBody { get; set; } | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
public Stream? RequestBody { get; internal set; } | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
public Stream? ResponseBody { get; internal set; } | ||
} |
112 changes: 112 additions & 0 deletions
112
tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<IDisposable> _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); | ||
} | ||
} |