Skip to content

Commit

Permalink
Allow for using query strings in DefaultProbingRequestFactory. (#2421)
Browse files Browse the repository at this point in the history
* Introduce a Query field on ActiveHealthCheckConfig. If provided, Query is appended to the probing request.

* <3 unit tests. Fix assignment in configurationconfigprovider.

* Add a test for a path with an embedded query string

* Mention the Query property in docs

---------

Co-authored-by: Robbie Knuth <jknuth@microsoft.com>
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
  • Loading branch information
3 people committed Mar 12, 2024
1 parent 9179027 commit 3c6ff2c
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 16 deletions.
3 changes: 2 additions & 1 deletion docs/docfx/articles/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ For additional fields see [ClusterConfig](xref:Yarp.ReverseProxy.Configuration.C
"Interval": "00:00:10",
"Timeout": "00:00:10",
"Policy": "ConsecutiveFailures",
"Path": "/api/health" // API endpoint to query for health state
"Path": "/api/health", // API endpoint to query for health state
"Query": "?foo=bar"
},
"Passive": { // Disables destinations based on HTTP response codes
"Enabled": true, // Defaults to false
Expand Down
7 changes: 5 additions & 2 deletions docs/docfx/articles/dests-health-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ There are several cluster-wide configuration settings controlling active health
"Interval": "00:00:10",
"Timeout": "00:00:10",
"Policy": "ConsecutiveFailures",
"Path": "/api/health"
"Path": "/api/health",
"Query": "?foo=bar",
}
},
"Metadata": {
Expand Down Expand Up @@ -49,7 +50,8 @@ var clusters = new[]
Interval = TimeSpan.FromSeconds(10),
Timeout = TimeSpan.FromSeconds(10),
Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures,
Path = "/api/health"
Path = "/api/health",
Query = "?foo=bar",
}
},
Metadata = new Dictionary<string, string> { { ConsecutiveFailuresHealthPolicyOptions.ThresholdMetadataName, "5" } },
Expand All @@ -74,6 +76,7 @@ Active health check settings can also be defined in code via the corresponding t
- `Timeout` - probing request timeout. Default `00:00:10`
- `Policy` - name of a policy evaluating destinations' active health states. Mandatory parameter
- `Path` - health check path on all cluster's destinations. Default `null`.
- `Query` - health check query on all cluster's destinations. Default `null`.

`Destination` section and [DestinationConfig](xref:Yarp.ReverseProxy.Configuration.DestinationConfig).

Expand Down
11 changes: 9 additions & 2 deletions src/ReverseProxy/Configuration/ActiveHealthCheckConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public sealed record ActiveHealthCheckConfig
/// </summary>
public string? Path { get; init; }

/// <summary>
/// Query string to append to the probe, including the leading '?'.
/// </summary>
public string? Query { get; init; }

public bool Equals(ActiveHealthCheckConfig? other)
{
if (other is null)
Expand All @@ -46,7 +51,8 @@ public bool Equals(ActiveHealthCheckConfig? other)
&& Interval == other.Interval
&& Timeout == other.Timeout
&& string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Path, other.Path, StringComparison.Ordinal);
&& string.Equals(Path, other.Path, StringComparison.Ordinal)
&& string.Equals(Query, other.Query, StringComparison.Ordinal);
}

public override int GetHashCode()
Expand All @@ -55,6 +61,7 @@ public override int GetHashCode()
Interval,
Timeout,
Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase),
Path?.GetHashCode(StringComparison.Ordinal));
Path?.GetHashCode(StringComparison.Ordinal),
Query?.GetHashCode(StringComparison.Ordinal));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ private static RouteQueryParameter CreateRouteQueryParameter(IConfigurationSecti
Interval = section.ReadTimeSpan(nameof(ActiveHealthCheckConfig.Interval)),
Timeout = section.ReadTimeSpan(nameof(ActiveHealthCheckConfig.Timeout)),
Policy = section[nameof(ActiveHealthCheckConfig.Policy)],
Path = section[nameof(ActiveHealthCheckConfig.Path)]
Path = section[nameof(ActiveHealthCheckConfig.Path)],
Query = section[nameof(ActiveHealthCheckConfig.Query)]
};
}

Expand Down
4 changes: 3 additions & 1 deletion src/ReverseProxy/Health/DefaultProbingRequestFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Net;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Net.Http.Headers;
using Yarp.ReverseProxy.Model;
Expand All @@ -20,7 +21,8 @@ public HttpRequestMessage CreateRequest(ClusterModel cluster, DestinationModel d
var probeAddress = !string.IsNullOrEmpty(destination.Config.Health) ? destination.Config.Health : destination.Config.Address;
var probePath = cluster.Config.HealthCheck?.Active?.Path;
UriHelper.FromAbsolute(probeAddress, out var destinationScheme, out var destinationHost, out var destinationPathBase, out _, out _);
var probeUri = UriHelper.BuildAbsolute(destinationScheme, destinationHost, destinationPathBase, probePath, default);
var query = QueryString.FromUriComponent(cluster.Config.HealthCheck?.Active?.Query ?? "");
var probeUri = UriHelper.BuildAbsolute(destinationScheme, destinationHost, destinationPathBase, probePath, query);

var request = new HttpRequestMessage(HttpMethod.Get, probeUri)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ public void Equals_Different_Value_Returns_False()
Assert.False(equals);
}

[Fact]
public void Equals_DifferingQueries_Returns_False()
{
var options1 = new ActiveHealthCheckConfig
{
Query = "?key=value1"
};

var options2 = new ActiveHealthCheckConfig
{
Query = "?key=value2"
};

var equals = options1.Equals(options2);

Assert.False(equals);
}

[Fact]
public void Equals_Second_Null_Returns_False()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ public class ConfigurationConfigProviderTests
Interval = TimeSpan.FromSeconds(4),
Timeout = TimeSpan.FromSeconds(6),
Policy = "Any5xxResponse",
Path = "healthCheckPath"
Path = "healthCheckPath",
Query = "?key=value"
},
AvailableDestinationsPolicy = "HealthyOrPanic"
},
Expand Down Expand Up @@ -239,7 +240,8 @@ public class ConfigurationConfigProviderTests
""Interval"": ""00:00:04"",
""Timeout"": ""00:00:06"",
""Policy"": ""Any5xxResponse"",
""Path"": ""healthCheckPath""
""Path"": ""healthCheckPath"",
""Query"": ""?key=value""
},
""AvailableDestinationsPolicy"": ""HealthyOrPanic""
},
Expand Down Expand Up @@ -540,6 +542,7 @@ private void VerifyValidAbstractConfig(IProxyConfig validConfig, IProxyConfig ab
Assert.Equal(cluster1.HealthCheck.Active.Timeout, abstractCluster1.HealthCheck.Active.Timeout);
Assert.Equal(cluster1.HealthCheck.Active.Policy, abstractCluster1.HealthCheck.Active.Policy);
Assert.Equal(cluster1.HealthCheck.Active.Path, abstractCluster1.HealthCheck.Active.Path);
Assert.Equal(cluster1.HealthCheck.Active.Query, abstractCluster1.HealthCheck.Active.Query);
Assert.Equal(LoadBalancingPolicies.Random, abstractCluster1.LoadBalancingPolicy);
Assert.Equal(cluster1.SessionAffinity.Enabled, abstractCluster1.SessionAffinity.Enabled);
Assert.Equal(cluster1.SessionAffinity.FailurePolicy, abstractCluster1.SessionAffinity.FailurePolicy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,28 @@ namespace Yarp.ReverseProxy.Health.Tests;
public class DefaultProbingRequestFactoryTests
{
[Theory]
[InlineData("https://localhost:10000/", null, null, "https://localhost:10000/")]
[InlineData("https://localhost:10000/", "https://localhost:20000/", null, "https://localhost:20000/")]
[InlineData("https://localhost:10000/", null, "/api/health/", "https://localhost:10000/api/health/")]
[InlineData("https://localhost:10000/", "https://localhost:20000/", "/api/health/", "https://localhost:20000/api/health/")]
[InlineData("https://localhost:10000/api", "https://localhost:20000/", "/health/", "https://localhost:20000/health/")]
[InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health/", "https://localhost:20000/api/health/")]
public void CreateRequest_HealthEndpointIsNotDefined_UseDestinationAddress(string address, string health, string healthPath, string expectedRequestUri)
[InlineData("https://localhost:10000/", null, null, null, "https://localhost:10000/")]
[InlineData("https://localhost:10000/", "https://localhost:20000/", null, null, "https://localhost:20000/")]
[InlineData("https://localhost:10000/", null, "/api/health/", null, "https://localhost:10000/api/health/")]
[InlineData("https://localhost:10000/", "https://localhost:20000/", "/api/health/", null, "https://localhost:20000/api/health/")]
[InlineData("https://localhost:10000/api", "https://localhost:20000/", "/health/", null, "https://localhost:20000/health/")]
[InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health/", null, "https://localhost:20000/api/health/")]
[InlineData("https://localhost:10000/", null, null, "?key=value", "https://localhost:10000/?key=value")]
[InlineData("https://localhost:10000/", "https://localhost:20000/", null, "?key=value", "https://localhost:20000/?key=value")]
[InlineData("https://localhost:10000/", null, "/api/health/", "?key=value", "https://localhost:10000/api/health/?key=value")]
[InlineData("https://localhost:10000/", "https://localhost:20000/", "/api/health/", "?key=value", "https://localhost:20000/api/health/?key=value")]
[InlineData("https://localhost:10000/api", "https://localhost:20000/", "/health/", "?key=value", "https://localhost:20000/health/?key=value")]
[InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health/", "?key=value", "https://localhost:20000/api/health/?key=value")]
[InlineData("https://localhost:10000/", "https://localhost:20000/api", "/health?foo=bar", "?key=value", "https://localhost:20000/api/health%3Ffoo=bar?key=value")]
public void CreateRequest_HealthEndpointIsNotDefined_UseDestinationAddress(string address, string health, string healthPath, string query, string expectedRequestUri)
{
var clusterModel = GetClusterConfig("cluster0",
new ActiveHealthCheckConfig()
{
Enabled = true,
Policy = "policy",
Path = healthPath,
Query = query,
}, HttpVersion.Version20);
var destinationModel = new DestinationModel(new DestinationConfig { Address = address, Health = health });
var factory = new DefaultProbingRequestFactory();
Expand Down

0 comments on commit 3c6ff2c

Please sign in to comment.