-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add BodyInspectionHandler (#493)
* feat: add BodyInspectionHandler Closes #482 * Fix tracing names * Register in default list * Add test to make sure request is still valid * Skip copy when non-seekable stream * dotnet format * configureAwait + cancellationToken * Fix cancellation token * Test octet request stream * Use Stream.Null instead of null and fix net462 issues * bump version and release notes --------- Co-authored-by: Andrew Omondi <[email protected]>
- Loading branch information
1 parent
b5bb2cd
commit 3bc8cbd
Showing
6 changed files
with
305 additions
and
3 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
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
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
109 changes: 109 additions & 0 deletions
109
src/http/httpClient/Middleware/BodyInspectionHandler.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,109 @@ | ||
// ------------------------------------------------------------------------------ | ||
// 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(BodyInspectionHandler)}_{nameof(SendAsync)}" | ||
); | ||
activity?.SetTag("com.microsoft.kiota.handler.bodyInspection.enable", true); | ||
} | ||
else | ||
{ | ||
activity = null; | ||
} | ||
try | ||
{ | ||
if(options.InspectRequestBody) | ||
{ | ||
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); | ||
} | ||
|
||
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, | ||
CancellationToken cancellationToken | ||
) | ||
{ | ||
if(httpContent is null or { Headers.ContentLength: 0 }) | ||
{ | ||
return Stream.Null; | ||
} | ||
|
||
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) | ||
{ | ||
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. 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. | ||
/// </summary> | ||
public Stream RequestBody { get; internal set; } = Stream.Null; | ||
|
||
/// <summary> | ||
/// Gets the response body stream for the current request. This stream is available | ||
/// 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. | ||
/// </summary> | ||
public Stream ResponseBody { get; internal set; } = Stream.Null; | ||
} |
138 changes: 138 additions & 0 deletions
138
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,138 @@ | ||
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.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 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.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() | ||
{ | ||
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.Same(Stream.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.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 BodyInspectionHandlerGetsNullResponseBodyStreamWhenThereIsNoResponseBody() | ||
{ | ||
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.Same(Stream.Null, 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); | ||
} | ||
} |