Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HeaderPropagation: propagate incoming request headers to outgoing HTTP requests #7921

Merged
merged 32 commits into from Mar 29, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
15c0684
Ported HeaderPropagation from aspnet/Extensions
alefranz Feb 25, 2019
3729951
Introduced Middleware
alefranz Feb 25, 2019
91a4970
Refactored middleware logic
alefranz Mar 10, 2019
eef7598
Refactored builder extensions
alefranz Mar 10, 2019
27bdc5c
Copyright notice
alefranz Mar 10, 2019
3262f6d
Test for friendly exception on Builder
alefranz Mar 10, 2019
d455d66
Fixed header name selection when no output name specified
alefranz Mar 10, 2019
cb6e356
Set comparer for the dictionary of headers
alefranz Mar 10, 2019
8a8b005
Merge branch 'master' into header-propagation
alefranz Mar 13, 2019
b0b67ef
Refactored configuration as Dictionary
alefranz Mar 13, 2019
3edbc95
Renamed state objects
alefranz Mar 13, 2019
0582f29
renamed OutboundHeaderName in configuration
alefranz Mar 13, 2019
f883f68
Changed DefaultValuesGenerator to ValueFactory
alefranz Mar 14, 2019
5af6d22
Missing docs
alefranz Mar 14, 2019
1d4c2f0
Removed AlwaysAdd and added tests for null entry in configuration
alefranz Mar 14, 2019
3e51de0
Improved docs
alefranz Mar 17, 2019
559491f
Merge branch 'master' into header-propagation
alefranz Mar 27, 2019
1619bb4
Update src/Middleware/HeaderPropagation/src/DependencyInjection/Heade…
rynowak Mar 27, 2019
4b414b0
Moved dependency injection extensions
alefranz Mar 27, 2019
ae94fe0
DI: reused ServiceCollection extension in the HttpClientBuilder one
alefranz Mar 27, 2019
78d4eec
Moved service registration
alefranz Mar 27, 2019
d06b743
Update src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs
rynowak Mar 27, 2019
adc5962
more docs
alefranz Mar 27, 2019
158faf6
Improved docs
alefranz Mar 27, 2019
aeee1fe
Update src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs
rynowak Mar 27, 2019
9706db1
Fixed build
alefranz Mar 27, 2019
c4de7f7
Update eng/SharedFramework.Local.props
analogrelay Mar 27, 2019
6e40837
Updated tests for null config
alefranz Mar 27, 2019
85d67e0
Reversed condition on HeaderPropagationMessageHandler as suggested
alefranz Mar 27, 2019
adab2b9
Added docs for HeaderPropagationMessageHandler
alefranz Mar 27, 2019
61df695
Changed proj to ship package to NuGet
alefranz Mar 28, 2019
689f594
Merge branch 'master' into header-propagation
alefranz Mar 29, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Expand Up @@ -73,6 +73,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.Abstractions" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.Abstractions\src\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.Abstractions\ref\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.EntityFrameworkCore\ref\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics\src\Microsoft.AspNetCore.Diagnostics.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics\ref\Microsoft.AspNetCore.Diagnostics.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.HeaderPropagation" ProjectPath="$(RepositoryRoot)src\Middleware\HeaderPropagation\src\Microsoft.AspNetCore.HeaderPropagation.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\HeaderPropagation\ref\Microsoft.AspNetCore.HeaderPropagation.csproj" />
<ProjectReferenceProvider Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" ProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks.EntityFrameworkCore\src\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks.EntityFrameworkCore\ref\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" ProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks\src\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks\ref\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.HostFiltering" ProjectPath="$(RepositoryRoot)src\Middleware\HostFiltering\src\Microsoft.AspNetCore.HostFiltering.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\HostFiltering\ref\Microsoft.AspNetCore.HostFiltering.csproj" />
Expand Down
1 change: 1 addition & 0 deletions eng/SharedFramework.Local.props
Expand Up @@ -56,6 +56,7 @@
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Cors" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Diagnostics" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.HeaderPropagation" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove this from the shared framework? We're generally very very cautious about what we put in there since it's very difficult to remove things later on. We're trying to keep it to things that every single app will use (moving forward at least).

We can still ship it as an official package as part of our release, it just wouldn't be included in the shared framework.

@dougbu Do you know if we need to do anything special to make sure this package is in the set of packages that will end up on NuGet? If we remove it from the shared framework, I want to make sure we know it will end up landing on nuget.org

Suggested change
<AspNetCoreAppReference Include="Microsoft.AspNetCore.HeaderPropagation" />

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember exactly. Removing the <AspNetCoreAppReference /> item may be sufficient. If not, there should be a project property you can set…

Copy link
Contributor Author

@alefranz alefranz Mar 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed IsAspNetCoreApp ans set IsShippingPackage in 61df695.
Looking at https://github.com/aspnet/AspNetCore/blob/master/Directory.Build.targets#L5-L17 this should be the correct approach and executing dotnet pack locally creates the package in the Shipping folder.

Can you please confirm @dougbu ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be working fine 😺 And, the CI build logs contain the following which also looks right:

  Successfully created package 'F:\workspace\_work\1\s\artifacts\packages\Release\Shipping\Microsoft.AspNetCore.HeaderPropagation.3.0.0-preview4-19177-46.nupkg'.
  Successfully created package 'F:\workspace\_work\1\s\artifacts\packages\Release\Shipping\Microsoft.AspNetCore.HeaderPropagation.3.0.0-preview4-19177-46.symbols.nupkg'.

Unfortunately, this is a fork and so artifacts aren't uploaded. Can't double-check that the shared framework bundles lack Microsoft.AspNetCore.HeaderPropagation.dll until this is merged. That's also fine because we can look at what gets published after this is merged.

<AspNetCoreAppReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.HostFiltering" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.HttpOverrides" />
Expand Down
@@ -0,0 +1,38 @@
using System;
rynowak marked this conversation as resolved.
Show resolved Hide resolved
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.HeaderPropagation
{
public static class HeaderPropagationExtensions
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
public static IServiceCollection AddHeaderPropagation(this IServiceCollection services, Action<HeaderPropagationOptions> configure)
{
services.TryAddSingleton<HeaderPropagationState>();
services.Configure(configure);

return services;
}

public static IHttpClientBuilder AddHeaderPropagation(this IHttpClientBuilder builder)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
builder.Services.TryAddSingleton<HeaderPropagationState>();
builder.Services.TryAddTransient<HeaderPropagationMessageHandler>();
rynowak marked this conversation as resolved.
Show resolved Hide resolved

builder.AddHttpMessageHandler(services =>
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
var state = services.GetRequiredService<HeaderPropagationState>();
return new HeaderPropagationMessageHandler(services.GetRequiredService<IOptions<HeaderPropagationOptions>>(), state);
});

return builder;
}

public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder builder)
{
return builder.UseMiddleware<HeaderPropagationMiddleware>();
rynowak marked this conversation as resolved.
Show resolved Hide resolved
}
}
rynowak marked this conversation as resolved.
Show resolved Hide resolved
}
15 changes: 15 additions & 0 deletions src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs
@@ -0,0 +1,15 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation
{
public class HeaderPropagationEntry
{
public string InputName { get; set; }
public string OutputName { get; set; }
rynowak marked this conversation as resolved.
Show resolved Hide resolved
public StringValues DefaultValues { get; set; }
rynowak marked this conversation as resolved.
Show resolved Hide resolved
public Func<HttpContext, StringValues> DefaultValuesGenerator { get; set; }
public bool AlwaysAdd { get; set; }
rynowak marked this conversation as resolved.
Show resolved Hide resolved
}
}
@@ -0,0 +1,43 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation
{
public class HeaderPropagationMessageHandler : DelegatingHandler
{
private readonly HeaderPropagationState _state;
private readonly HeaderPropagationOptions _options;

public HeaderPropagationMessageHandler(IOptions<HeaderPropagationOptions> options, HeaderPropagationState state)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

_options = options.Value;

_state = state ?? throw new ArgumentNullException(nameof(state));
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
foreach (var header in _options.Headers)
{
if (_state.Headers.TryGetValue(header.OutputName, out var values) &&
!StringValues.IsNullOrEmpty(values) &&
rynowak marked this conversation as resolved.
Show resolved Hide resolved
(header.AlwaysAdd || !request.Headers.Contains(header.OutputName)))
{
request.Headers.TryAddWithoutValidation(header.OutputName, (string[]) values);
}
}

return base.SendAsync(request, cancellationToken);
}
}
}
@@ -0,0 +1,60 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation
{
public class HeaderPropagationMiddleware
{
private readonly RequestDelegate _next;
private readonly HeaderPropagationOptions _options;
private readonly HeaderPropagationState _state;

public HeaderPropagationMiddleware(RequestDelegate next, IOptions<HeaderPropagationOptions> options, HeaderPropagationState state)
{
_next = next ?? throw new ArgumentNullException(nameof(next));

if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_options = options.Value;

_state = state ?? throw new ArgumentNullException(nameof(state));
}

public Task Invoke(HttpContext context)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
if (context != null)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
foreach (var header in _options.Headers)
{
if (!context.Request.Headers.TryGetValue(header.InputName, out var values)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
|| StringValues.IsNullOrEmpty(values))
{
if (header.DefaultValuesGenerator != null)
{
values = header.DefaultValuesGenerator(context);
if (StringValues.IsNullOrEmpty(values)) continue;
}
else if (!StringValues.IsNullOrEmpty(header.DefaultValues))
{
values = header.DefaultValues;
}
else
{
continue;
}
}

var outputName = !string.IsNullOrEmpty(header.OutputName) ? header.OutputName : header.InputName;
rynowak marked this conversation as resolved.
Show resolved Hide resolved
_state.Headers.TryAdd(outputName, values);
}
}

return _next.Invoke(context);
}
}
}
@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace Microsoft.AspNetCore.HeaderPropagation
{
public class HeaderPropagationOptions
{
public IList<HeaderPropagationEntry> Headers { get; set; } = new List<HeaderPropagationEntry>();
}
}
rynowak marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 19 additions & 0 deletions src/Middleware/HeaderPropagation/src/HeaderPropagationState.cs
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation
{
public class HeaderPropagationState
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
private static AsyncLocal<Dictionary<string, StringValues>> _headers { get; } = new AsyncLocal<Dictionary<string, StringValues>>();
rynowak marked this conversation as resolved.
Show resolved Hide resolved

public Dictionary<string, StringValues> Headers
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
get
{
return _headers.Value ?? (_headers.Value = new Dictionary<string, StringValues>());
rynowak marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>ASP.NET Core middleware to propagate HTTP headers from the incoming request to the outgoing HTTP Client requests</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;httpclient</PackageTags>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.HeaderPropagation.Tests" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.Extensions.Http" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

</Project>
@@ -0,0 +1,94 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Microsoft.AspNetCore.HeaderPropagation.Tests
{
public class HeaderPropagationIntegrationTest
{
[Fact]
public async Task HeaderInRequest_AddCorrectValue()
{
// Arrange
var handler = new SimpleHandler();
var builder = CreateBuilder(c =>
c.Headers.Add(new HeaderPropagationEntry
{
InputName = "in",
OutputName = "out",
}),
handler);
var server = new TestServer(builder);
var client = server.CreateClient();

var request = new HttpRequestMessage();
request.Headers.Add("in", "test");

// Act
var response = await client.SendAsync(request);

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(handler.Headers.Contains("out"));
Assert.Equal(new[] { "test" }, handler.Headers.GetValues("out"));
}

private IWebHostBuilder CreateBuilder(Action<HeaderPropagationOptions> configure, HttpMessageHandler primaryHandler)
{
return new WebHostBuilder()
.Configure(app =>
{
app.UseHeaderPropagation();
app.Map("", x => x.UseMiddleware<SimpleMiddleware>());
rynowak marked this conversation as resolved.
Show resolved Hide resolved
})
.ConfigureServices(services =>
{
services.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpMessageHandlerBuilder(b =>
{
b.PrimaryHandler = primaryHandler;
})
.AddHeaderPropagation();
services.AddHeaderPropagation(configure);
});
}

private class SimpleHandler : DelegatingHandler
{
public HttpHeaders Headers { get; private set; }

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Headers = request.Headers;
return Task.FromResult(new HttpResponseMessage());
}
}

private class SimpleMiddleware
{
private readonly RequestDelegate _next;
private readonly IHttpClientFactory _httpClientFactory;

public SimpleMiddleware(RequestDelegate next, IHttpClientFactory httpClientFactory)
{
_next = next;
_httpClientFactory = httpClientFactory;
}

public Task InvokeAsync(HttpContext _)
{
var client = _httpClientFactory.CreateClient("example.com");
return client.GetAsync("");
}
}
}
}