Skip to content

Commit

Permalink
feat: add BodyInspectionHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
marcinjahn committed Dec 7, 2024
1 parent 48ac98b commit 74e5e5d
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 0 deletions.
95 changes: 95 additions & 0 deletions src/http/httpClient/Middleware/BodyInspectionHandler.cs
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;
}
}
}
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 tests/http/httpClient/Middleware/BodyInspectionHandlerTests.cs
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);
}
}

0 comments on commit 74e5e5d

Please sign in to comment.