Skip to content
This repository was archived by the owner on Jul 9, 2024. It is now read-only.

Commit 8449b7e

Browse files
authored
Merge pull request #133 from microsoft/feature/header-observe
feature/header observe
2 parents 76c2da5 + 5442a4b commit 8449b7e

File tree

9 files changed

+230
-20
lines changed

9 files changed

+230
-20
lines changed

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"cSpell.words": [
33
"kiota"
4-
]
4+
],
5+
"dotnet.defaultSolution": "Microsoft.Kiota.Http.HttpClientLibrary.sln",
6+
"editor.formatOnSave": true
57
}

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.1.0] - 2023-08-11
11+
12+
### Added
13+
14+
- Added headers inspection handler to allow clients to observe request and response headers.
15+
1016
## [1.0.6] - 2023-07-06
1117

12-
- Fixes a bug where empty streams would be passed to the serializers if the response content header is set.
18+
- Fixes a bug where empty streams would be passed to the serializers if the response content header is set.
1319

1420
## [1.0.5] - 2023-06-29
1521

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net.Http;
5+
using System.Threading.Tasks;
6+
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
7+
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
8+
using Microsoft.Kiota.Http.HttpClientLibrary.Tests.Mocks;
9+
using Xunit;
10+
11+
namespace Microsoft.Kiota.Http.HttpClientLibrary.Tests.Middleware;
12+
public class HeadersInspectionHandlerTests : IDisposable
13+
{
14+
private readonly List<IDisposable> _disposables = new();
15+
[Fact]
16+
public void HeadersInspectionHandlerConstruction()
17+
{
18+
using var defaultValue = new HeadersInspectionHandler();
19+
Assert.NotNull(defaultValue);
20+
}
21+
22+
[Fact]
23+
public async Task HeadersInspectionHandlerGetsRequestHeaders()
24+
{
25+
var option = new HeadersInspectionHandlerOption
26+
{
27+
InspectRequestHeaders = true,
28+
};
29+
using var invoker = GetMessageInvoker(option);
30+
31+
// When
32+
var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost");
33+
request.Headers.Add("test", "test");
34+
var response = await invoker.SendAsync(request, default);
35+
36+
// Then
37+
Assert.Equal("test", option.RequestHeaders["test"].First());
38+
Assert.Empty(option.ResponseHeaders);
39+
}
40+
[Fact]
41+
public async Task HeadersInspectionHandlerGetsResponseHeaders()
42+
{
43+
var option = new HeadersInspectionHandlerOption
44+
{
45+
InspectResponseHeaders = true,
46+
};
47+
using var invoker = GetMessageInvoker(option);
48+
49+
// When
50+
var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost");
51+
var response = await invoker.SendAsync(request, default);
52+
53+
// Then
54+
Assert.Equal("test", option.ResponseHeaders["test"].First());
55+
Assert.Empty(option.RequestHeaders);
56+
}
57+
private HttpMessageInvoker GetMessageInvoker(HeadersInspectionHandlerOption option = null)
58+
{
59+
var messageHandler = new MockRedirectHandler();
60+
_disposables.Add(messageHandler);
61+
var response = new HttpResponseMessage();
62+
response.Headers.Add("test", "test");
63+
_disposables.Add(response);
64+
messageHandler.SetHttpResponse(response);
65+
// Given
66+
var handler = new HeadersInspectionHandler(option)
67+
{
68+
InnerHandler = messageHandler
69+
};
70+
_disposables.Add(handler);
71+
return new HttpMessageInvoker(handler);
72+
}
73+
74+
public void Dispose()
75+
{
76+
_disposables.ForEach(static x => x.Dispose());
77+
GC.SuppressFinalize(this);
78+
}
79+
}

Microsoft.Kiota.Http.HttpClientLibrary.Tests/Middleware/RetryHandlerTests.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@ public class RetryHandlerTests : IDisposable
2525

2626
public RetryHandlerTests()
2727
{
28-
this._testHttpMessageHandler = new MockRedirectHandler();
29-
this._retryHandler = new RetryHandler
28+
_testHttpMessageHandler = new MockRedirectHandler();
29+
_retryHandler = new RetryHandler
3030
{
31-
InnerHandler = this._testHttpMessageHandler
31+
InnerHandler = _testHttpMessageHandler
3232
};
33-
this._invoker = new HttpMessageInvoker(this._retryHandler);
33+
_invoker = new HttpMessageInvoker(_retryHandler);
3434
}
3535

3636
public void Dispose()
3737
{
38-
this._invoker.Dispose();
38+
_invoker.Dispose();
3939
GC.SuppressFinalize(this);
4040
}
4141

@@ -81,9 +81,9 @@ public async Task OkStatusShouldPassThrough()
8181
// Arrange
8282
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://example.org/foo");
8383
var retryResponse = new HttpResponseMessage(HttpStatusCode.OK);
84-
this._testHttpMessageHandler.SetHttpResponse(retryResponse);
84+
_testHttpMessageHandler.SetHttpResponse(retryResponse);
8585
// Act
86-
var response = await this._invoker.SendAsync(httpRequestMessage, new CancellationToken());
86+
var response = await _invoker.SendAsync(httpRequestMessage, new CancellationToken());
8787
// Assert
8888
Assert.Same(response, retryResponse);
8989
Assert.NotNull(response.RequestMessage);
@@ -247,7 +247,7 @@ public async Task ShouldDelayBasedOnRetryAfterHeaderWithHttpDate(HttpStatusCode
247247
var retryResponse = new HttpResponseMessage(statusCode);
248248
var futureTime = DateTime.Now + TimeSpan.FromSeconds(3);// 3 seconds from now
249249
var futureTimeString = futureTime.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern);
250-
Assert.Contains("GMT",futureTimeString); // http date always end in GMT according to the spec
250+
Assert.Contains("GMT", futureTimeString); // http date always end in GMT according to the spec
251251
retryResponse.Headers.TryAddWithoutValidation(RetryAfter, futureTimeString);
252252
// Act
253253
await DelayTestWithMessage(retryResponse, 1, "Init");

src/KiotaClientFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public static IList<DelegatingHandler> CreateDefaultHandlers()
4040
new RedirectHandler(),
4141
new ParametersNameDecodingHandler(),
4242
new UserAgentHandler(),
43+
new HeadersInspectionHandler(),
4344
};
4445
}
4546
/// <summary>

src/Microsoft.Kiota.Http.HttpClientLibrary.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<PackageProjectUrl>https://aka.ms/kiota/docs</PackageProjectUrl>
1515
<EmbedUntrackedSources>true</EmbedUntrackedSources>
1616
<Deterministic>true</Deterministic>
17-
<VersionPrefix>1.0.6</VersionPrefix>
17+
<VersionPrefix>1.1.0</VersionPrefix>
1818
<VersionSuffix></VersionSuffix>
1919
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
2020
<!-- Enable this line once we go live to prevent breaking changes -->
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
using System;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
using System.Net.Http;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.Kiota.Http.HttpClientLibrary.Extensions;
12+
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
13+
14+
namespace Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
15+
16+
/// <summary>
17+
/// The Headers Inspection Handler allows the developer to inspect the headers of the request and response.
18+
/// </summary>
19+
public class HeadersInspectionHandler : DelegatingHandler
20+
{
21+
private readonly HeadersInspectionHandlerOption _defaultOptions;
22+
/// <summary>
23+
/// Create a new instance of <see cref="HeadersInspectionHandler"/>
24+
/// </summary>
25+
/// <param name="defaultOptions">Default options to apply to the handler</param>
26+
public HeadersInspectionHandler(HeadersInspectionHandlerOption? defaultOptions = null)
27+
{
28+
_defaultOptions = defaultOptions ?? new HeadersInspectionHandlerOption();
29+
}
30+
/// <inheritdoc/>
31+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
32+
{
33+
if(request == null) throw new ArgumentNullException(nameof(request));
34+
35+
var options = request.GetRequestOption<HeadersInspectionHandlerOption>() ?? _defaultOptions;
36+
37+
ActivitySource? activitySource;
38+
Activity? activity;
39+
if(request.GetRequestOption<ObservabilityOptions>() is ObservabilityOptions obsOptions)
40+
{
41+
activitySource = new ActivitySource(obsOptions.TracerInstrumentationName);
42+
activity = activitySource?.StartActivity($"{nameof(RedirectHandler)}_{nameof(SendAsync)}");
43+
activity?.SetTag("com.microsoft.kiota.handler.headersInspection.enable", true);
44+
}
45+
else
46+
{
47+
activity = null;
48+
activitySource = null;
49+
}
50+
try
51+
{
52+
if(options.InspectRequestHeaders)
53+
{
54+
foreach(var header in request.Headers)
55+
{
56+
options.RequestHeaders[header.Key] = header.Value.ToArray();
57+
}
58+
if(request.Content != null)
59+
foreach(var contentHeaders in request.Content.Headers)
60+
{
61+
options.RequestHeaders[contentHeaders.Key] = contentHeaders.Value.ToArray();
62+
}
63+
}
64+
var response = await base.SendAsync(request, cancellationToken);
65+
if(options.InspectResponseHeaders)
66+
{
67+
foreach(var header in response.Headers)
68+
{
69+
options.ResponseHeaders[header.Key] = header.Value.ToArray();
70+
}
71+
if(response.Content != null)
72+
foreach(var contentHeaders in response.Content.Headers)
73+
{
74+
options.ResponseHeaders[contentHeaders.Key] = contentHeaders.Value.ToArray();
75+
}
76+
}
77+
return response;
78+
}
79+
finally
80+
{
81+
activity?.Dispose();
82+
activitySource?.Dispose();
83+
}
84+
85+
}
86+
87+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
using Microsoft.Kiota.Abstractions;
6+
7+
namespace Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
8+
/// <summary>
9+
/// The Headers Inspection Option allows the developer to inspect the headers of the request and response.
10+
/// </summary>
11+
public class HeadersInspectionHandlerOption : IRequestOption
12+
{
13+
/// <summary>
14+
/// Gets or sets a value indicating whether the request headers should be inspected.
15+
/// </summary>
16+
public bool InspectRequestHeaders
17+
{
18+
get; set;
19+
}
20+
/// <summary>
21+
/// Gets or sets a value indicating whether the response headers should be inspected.
22+
/// </summary>
23+
public bool InspectResponseHeaders
24+
{
25+
get; set;
26+
}
27+
/// <summary>
28+
/// Gets the request headers to for the current request.
29+
/// </summary>
30+
public RequestHeaders RequestHeaders { get; private set; } = new RequestHeaders();
31+
/// <summary>
32+
/// Gets the response headers for the current request.
33+
/// </summary>
34+
public RequestHeaders ResponseHeaders { get; private set; } = new RequestHeaders();
35+
}

src/Middleware/RedirectHandler.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public RedirectHandler(RedirectHandlerOption? redirectOption = null)
3232
/// </summary>
3333
internal RedirectHandlerOption RedirectOption
3434
{
35-
get; set;
35+
get; private set;
3636
}
3737

3838
/// <summary>
@@ -45,7 +45,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
4545
{
4646
if(request == null) throw new ArgumentNullException(nameof(request));
4747

48-
RedirectOption = request.GetRequestOption<RedirectHandlerOption>() ?? RedirectOption;
48+
var redirectOption = request.GetRequestOption<RedirectHandlerOption>() ?? RedirectOption;
4949

5050
ActivitySource? activitySource;
5151
Activity? activity;
@@ -63,7 +63,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
6363
var response = await base.SendAsync(request, cancellationToken);
6464

6565
// check response status code and redirect handler option
66-
if(ShouldRedirect(response))
66+
if(ShouldRedirect(response, redirectOption))
6767
{
6868
if(response.Headers.Location == null)
6969
{
@@ -74,7 +74,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
7474

7575
var redirectCount = 0;
7676

77-
while(redirectCount < RedirectOption.MaxRedirect)
77+
while(redirectCount < redirectOption.MaxRedirect)
7878
{
7979
using var redirectActivity = activitySource?.StartActivity($"{nameof(RedirectHandler)}_{nameof(SendAsync)} - redirect {redirectCount}");
8080
redirectActivity?.SetTag("com.microsoft.kiota.handler.redirect.count", redirectCount);
@@ -119,18 +119,18 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
119119
}
120120

121121
// If scheme has changed. Ensure that this has been opted in for security reasons
122-
if(!newRequest.RequestUri.Scheme.Equals(request.RequestUri?.Scheme) && !RedirectOption.AllowRedirectOnSchemeChange)
122+
if(!newRequest.RequestUri.Scheme.Equals(request.RequestUri?.Scheme) && !redirectOption.AllowRedirectOnSchemeChange)
123123
{
124124
throw new InvalidOperationException(
125-
$"Redirects with changing schemes not allowed by default. You can change this by modifying the {nameof(RedirectOption.AllowRedirectOnSchemeChange)} option",
125+
$"Redirects with changing schemes not allowed by default. You can change this by modifying the {nameof(redirectOption.AllowRedirectOnSchemeChange)} option",
126126
new Exception($"Scheme changed from {request.RequestUri?.Scheme} to {newRequest.RequestUri.Scheme}."));
127127
}
128128

129129
// Send redirect request to get response
130130
response = await base.SendAsync(newRequest, cancellationToken);
131131

132132
// Check response status code
133-
if(ShouldRedirect(response))
133+
if(ShouldRedirect(response, redirectOption))
134134
{
135135
redirectCount++;
136136
}
@@ -151,9 +151,9 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
151151
}
152152
}
153153

154-
private bool ShouldRedirect(HttpResponseMessage responseMessage)
154+
private bool ShouldRedirect(HttpResponseMessage responseMessage, RedirectHandlerOption redirectOption)
155155
{
156-
return IsRedirect(responseMessage.StatusCode) && RedirectOption.ShouldRedirect(responseMessage) && RedirectOption.MaxRedirect > 0;
156+
return IsRedirect(responseMessage.StatusCode) && redirectOption.ShouldRedirect(responseMessage) && redirectOption.MaxRedirect > 0;
157157
}
158158

159159
/// <summary>

0 commit comments

Comments
 (0)