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 all 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Net.Http;
using Microsoft.AspNetCore.HeaderPropagation;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Builder
{
public static class HeaderPropagationApplicationBuilderExtensions
{
private static readonly string _unableToFindServices = string.Format(
"Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to 'ConfigureServices(...)' in the application startup code.",
nameof(IServiceCollection),
nameof(HeaderPropagationServiceCollectionExtensions.AddHeaderPropagation));

/// <summary>
/// Adds a middleware that collect headers to be propagated to a <see cref="HttpClient"/>.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}

if (app.ApplicationServices.GetService<HeaderPropagationValues>() == null)
{
throw new InvalidOperationException(_unableToFindServices);
}

return app.UseMiddleware<HeaderPropagationMiddleware>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.HeaderPropagation;

namespace Microsoft.Extensions.DependencyInjection
{
public static class HeaderPropagationHttpClientBuilderExtensions
{
/// <summary>
/// Adds a message handler for propagating headers collected by the <see cref="HeaderPropagationMiddleware"/> to a outgoing request.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/> to add the message handler to.</param>
/// <returns>The <see cref="IHttpClientBuilder"/> so that additional calls can be chained.</returns>
public static IHttpClientBuilder AddHeaderPropagation(this IHttpClientBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Services.AddHeaderPropagation();

builder.AddHttpMessageHandler<HeaderPropagationMessageHandler>();

return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Net.Http;
using Microsoft.AspNetCore.HeaderPropagation;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
public static class HeaderPropagationServiceCollectionExtensions
{
/// <summary>
/// Adds services required for propagating headers to a <see cref="HttpClient"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddHeaderPropagation(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

services.TryAddSingleton<HeaderPropagationValues>();
services.TryAddTransient<HeaderPropagationMessageHandler>();

return services;
}

/// <summary>
/// Adds services required for propagating headers to a <see cref="HttpClient"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="configureOptions">A delegate used to configure the <see cref="HeaderPropagationOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddHeaderPropagation(this IServiceCollection services, Action<HeaderPropagationOptions> configureOptions)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}

services.Configure(configureOptions);
services.AddHeaderPropagation();

return services;
}
}
}
56 changes: 56 additions & 0 deletions src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation
{
/// <summary>
/// Define the configuration of a header for the <see cref="HeaderPropagationMiddleware"/>.
/// </summary>
public class HeaderPropagationEntry
{
/// <summary>
/// Gets or sets the name of the header to be used by the <see cref="HeaderPropagationMessageHandler"/> for the
/// outbound http requests.
/// </summary>
/// <remarks>
/// If <see cref="ValueFactory"/> is present, the value of the header in the outbound calls will be the one
/// returned by the factory or, if the factory returns an empty value, the header will be omitted.
/// Otherwise, it will be the value of the header in the incoming request named as the key of this entry in
/// <see cref="HeaderPropagationOptions.Headers"/> or, if missing or empty, the value specified in
/// <see cref="DefaultValue"/> or, if the <see cref="DefaultValue"/> is empty, it will not be
/// added to the outbound calls.
/// </remarks>
public string OutboundHeaderName { get; set; }

rynowak marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Gets or sets the default value to be used when the header in the incoming request is missing or empty.
/// </summary>
rynowak marked this conversation as resolved.
Show resolved Hide resolved
/// <remarks>
/// This value is ignored when <see cref="ValueFactory"/> is set.
/// When it is <see cref="StringValues.Empty"/> it has no effect and, if the header is missing or empty in the
/// incoming request, it will not be added to outbound calls.
/// </remarks>
public StringValues DefaultValue { get; set; }

/// <summary>
/// Gets or sets the value factory to be used.
/// It gets as input the inbound header name for this entry as defined in
/// <see cref="HeaderPropagationOptions.Headers"/> and the <see cref="HttpContext"/> of the current request.
/// </summary>
/// <remarks>
/// When present, the factory is the only method used to set the value.
/// The factory should return <see cref="StringValues.Empty"/> to not add the header.
/// When not present, the value will be taken from the header in the incoming request named as the key of this
/// entry in <see cref="HeaderPropagationOptions.Headers"/> or, if missing or empty, it will be the values
/// specified in <see cref="DefaultValue"/> or, if the <see cref="DefaultValue"/> is empty, the header will not
/// be added to the outbound calls.
/// Please note the factory is called only once per incoming request and the same value will be used by all the
/// outbound calls.
/// </remarks>
rynowak marked this conversation as resolved.
Show resolved Hide resolved
public Func<string, HttpContext, StringValues> ValueFactory { get; set; }
rynowak marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

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
{
/// <summary>
/// A message handler for propagating headers collected by the <see cref="HeaderPropagationMiddleware"/> to a outgoing request.
/// </summary>
public class HeaderPropagationMessageHandler : DelegatingHandler
{
private readonly HeaderPropagationValues _values;
private readonly HeaderPropagationOptions _options;

/// <summary>
/// Creates a new instance of the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
/// <param name="options">The options that define which headers are propagated.</param>
/// <param name="values">The values of the headers to be propagated populated by the
/// <see cref="HeaderPropagationMiddleware"/>.</param>
public HeaderPropagationMessageHandler(IOptions<HeaderPropagationOptions> options, HeaderPropagationValues values)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

_options = options.Value;

_values = values ?? throw new ArgumentNullException(nameof(values));
}

/// <summary>
/// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation, after adding
/// the propagated headers.
/// </summary>
/// <remarks>
/// If an header with the same name is already present in the request, even if empty, the corresponding
/// propagated header will not be added.
/// </remarks>
/// <param name="request">The HTTP request message to send to the server.</param>
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
foreach ((var headerName, var entry) in _options.Headers)
{
var outputName = string.IsNullOrEmpty(entry?.OutboundHeaderName) ? headerName : entry.OutboundHeaderName;

if (!request.Headers.Contains(outputName) &&
_values.Headers.TryGetValue(headerName, out var values) &&
!StringValues.IsNullOrEmpty(values))
{
request.Headers.TryAddWithoutValidation(outputName, (string[])values);
Copy link
Member

Choose a reason for hiding this comment

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

The cast (string[])values will cause an array allocation for most headers (which are only one)

Better would be to check the count and then switch on overload e.g.

var count = values.Count;
if (count == 1)
{
    request.Headers.TryAddWithoutValidation(outputName, (string)values);
}
else if (count > 1)
{
    request.Headers.TryAddWithoutValidation(outputName, (string[])values);
}

Copy link
Member

Choose a reason for hiding this comment

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

Also HttpClient has an odd behaviour where some headers are rejected from the Headers collection and instead need to be added to the Content.Headers collection (if there is a content); so might need to be more like:

var hasContent = request.Content != null;

// ...

var count = values.Count;
if (count == 1)
{
    var value = (string)values;
    if (!request.Headers.TryAddWithoutValidation(outputName, value) && hasContent)
    {
        request.Content.Headers.TryAddWithoutValidation(outputName, value);
    }
} 
else ...

from: https://github.com/aspnet/Benchmarks/blob/7b1a5e986f06ae79270b13866e701cc6cb5bb395/src/Proxy/ProxyExtensions.cs#L31-L34

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @benaadams !
I'll raise a PR to address this by the end of the week.

}
}

return base.SendAsync(request, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation
{
/// <summary>
/// A Middleware for propagating headers to a <see cref="HttpClient"/>.
/// </summary>
public class HeaderPropagationMiddleware
{
private readonly RequestDelegate _next;
private readonly HeaderPropagationOptions _options;
private readonly HeaderPropagationValues _values;

public HeaderPropagationMiddleware(RequestDelegate next, IOptions<HeaderPropagationOptions> options, HeaderPropagationValues values)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
_next = next ?? throw new ArgumentNullException(nameof(next));

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

_values = values ?? throw new ArgumentNullException(nameof(values));
}

public Task Invoke(HttpContext context)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
foreach ((var headerName, var entry) in _options.Headers)
{
var values = GetValues(headerName, entry, context);

if (!StringValues.IsNullOrEmpty(values))
{
_values.Headers.TryAdd(headerName, values);
}
}

return _next.Invoke(context);
}

private static StringValues GetValues(string headerName, HeaderPropagationEntry entry, HttpContext context)
{
if (entry?.ValueFactory != null)
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
return entry.ValueFactory(headerName, context);
}

if (context.Request.Headers.TryGetValue(headerName, out var values)
&& !StringValues.IsNullOrEmpty(values))
{
return values;
}

return entry != null ? entry.DefaultValue : StringValues.Empty;
}
}
}
19 changes: 19 additions & 0 deletions src/Middleware/HeaderPropagation/src/HeaderPropagationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;

namespace Microsoft.AspNetCore.HeaderPropagation
{
/// <summary>
/// Provides configuration for the <see cref="HeaderPropagationMiddleware"/>.
/// </summary>
public class HeaderPropagationOptions
{
/// <summary>
/// Gets or sets the headers to be collected by the <see cref="HeaderPropagationMiddleware"/>
/// and to be propagated by the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
public IDictionary<string, HeaderPropagationEntry> Headers { get; set; } = new Dictionary<string, HeaderPropagationEntry>();
}
}
rynowak marked this conversation as resolved.
Show resolved Hide resolved
29 changes: 29 additions & 0 deletions src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation
{
/// <summary>
/// Contains the headers values for the <see cref="HeaderPropagationMiddleware"/>.
/// </summary>
public class HeaderPropagationValues
{
private readonly static AsyncLocal<Dictionary<string, StringValues>> _headers = new AsyncLocal<Dictionary<string, StringValues>>();

/// <summary>
/// Gets the headers values collected by the <see cref="HeaderPropagationMiddleware"/> from the current request that can be propagated.
/// </summary>
public IDictionary<string, StringValues> Headers
{
get
{
return _headers.Value ?? (_headers.Value = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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>
<IsShippingPackage>true</IsShippingPackage>
<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>
Loading