diff --git a/HttpClientInterception.sln b/HttpClientInterception.sln
deleted file mode 100644
index e0c1e175..00000000
--- a/HttpClientInterception.sln
+++ /dev/null
@@ -1,125 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31423.177
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{00F2B78C-D33E-4090-9888-C33435B02F1F}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1A2A0990-94DE-4F14-850B-39BA7183BC63}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- .gitattributes = .gitattributes
- .gitignore = .gitignore
- .vsconfig = .vsconfig
- build.ps1 = build.ps1
- Directory.Build.props = Directory.Build.props
- Directory.Build.targets = Directory.Build.targets
- Directory.Packages.props = Directory.Packages.props
- global.json = global.json
- HttpClientInterception.ruleset = HttpClientInterception.ruleset
- LICENSE = LICENSE
- NuGet.config = NuGet.config
- README.md = README.md
- SECURITY.md = SECURITY.md
- stylecop.json = stylecop.json
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6DC2A1B9-1D37-4062-A75D-6A03C7367A46}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JustEat.HttpClientInterception", "src\HttpClientInterception\JustEat.HttpClientInterception.csproj", "{ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JustEat.HttpClientInterception.Tests", "tests\HttpClientInterception.Tests\JustEat.HttpClientInterception.Tests.csproj", "{F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{58FB0A27-4361-45ED-BB97-FD836F5FEF24}"
- ProjectSection(SolutionItems) = preProject
- samples\README.md = samples\README.md
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "samples\SampleApp\SampleApp.csproj", "{A9B2AF56-BAD5-4099-AE88-37B71E72C20D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp.Tests", "samples\SampleApp.Tests\SampleApp.Tests.csproj", "{F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JustEat.HttpClientInterception.Benchmarks", "tests\HttpClientInterception.Benchmarks\JustEat.HttpClientInterception.Benchmarks.csproj", "{E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F258F815-A7C5-44F7-AF55-E12C0F1FB023}"
- ProjectSection(SolutionItems) = preProject
- .github\CODEOWNERS = .github\CODEOWNERS
- .github\CONTRIBUTING.md = .github\CONTRIBUTING.md
- .github\dependabot.yml = .github\dependabot.yml
- .github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md
- .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md
- .github\stale.yml = .github\stale.yml
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{9071A217-7882-4C3D-A7F6-3D7A56D6177D}"
- ProjectSection(SolutionItems) = preProject
- .github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md
- .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".vscode", ".vscode", "{D8A60EDB-E535-4DE2-9284-E758F1FDA350}"
- ProjectSection(SolutionItems) = preProject
- .vscode\launch.json = .vscode\launch.json
- .vscode\tasks.json = .vscode\tasks.json
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{8127648B-FBA5-487E-8583-0E4A374C65DC}"
- ProjectSection(SolutionItems) = preProject
- .github\workflows\approve-and-merge.yml = .github\workflows\approve-and-merge.yml
- .github\workflows\build.yml = .github\workflows\build.yml
- .github\workflows\bump-version.yml = .github\workflows\bump-version.yml
- .github\workflows\codeql.yml = .github\workflows\codeql.yml
- .github\workflows\dependabot-approve.yml = .github\workflows\dependabot-approve.yml
- .github\workflows\dependency-review.yml = .github\workflows\dependency-review.yml
- .github\workflows\lint.yml = .github\workflows\lint.yml
- .github\workflows\release.yml = .github\workflows\release.yml
- .github\workflows\scorecard.yml = .github\workflows\scorecard.yml
- .github\workflows\update-docs.yml = .github\workflows\update-docs.yml
- .github\workflows\update-dotnet-sdk.yml = .github\workflows\update-dotnet-sdk.yml
- EndProjectSection
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}.Release|Any CPU.Build.0 = Release|Any CPU
- {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}.Release|Any CPU.Build.0 = Release|Any CPU
- {A9B2AF56-BAD5-4099-AE88-37B71E72C20D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A9B2AF56-BAD5-4099-AE88-37B71E72C20D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A9B2AF56-BAD5-4099-AE88-37B71E72C20D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A9B2AF56-BAD5-4099-AE88-37B71E72C20D}.Release|Any CPU.Build.0 = Release|Any CPU
- {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}.Release|Any CPU.Build.0 = Release|Any CPU
- {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47} = {00F2B78C-D33E-4090-9888-C33435B02F1F}
- {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07} = {6DC2A1B9-1D37-4062-A75D-6A03C7367A46}
- {A9B2AF56-BAD5-4099-AE88-37B71E72C20D} = {58FB0A27-4361-45ED-BB97-FD836F5FEF24}
- {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3} = {58FB0A27-4361-45ED-BB97-FD836F5FEF24}
- {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8} = {6DC2A1B9-1D37-4062-A75D-6A03C7367A46}
- {F258F815-A7C5-44F7-AF55-E12C0F1FB023} = {1A2A0990-94DE-4F14-850B-39BA7183BC63}
- {9071A217-7882-4C3D-A7F6-3D7A56D6177D} = {F258F815-A7C5-44F7-AF55-E12C0F1FB023}
- {D8A60EDB-E535-4DE2-9284-E758F1FDA350} = {1A2A0990-94DE-4F14-850B-39BA7183BC63}
- {8127648B-FBA5-487E-8583-0E4A374C65DC} = {F258F815-A7C5-44F7-AF55-E12C0F1FB023}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {E4BC3B24-A96F-46C5-9405-CBE87A972881}
- EndGlobalSection
-EndGlobal
diff --git a/HttpClientInterception.slnx b/HttpClientInterception.slnx
new file mode 100644
index 00000000..e5a989bc
--- /dev/null
+++ b/HttpClientInterception.slnx
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/HttpClientInterception/HttpClientInterceptorOptions.cs b/src/HttpClientInterception/HttpClientInterceptorOptions.cs
index 1bc4f464..b286cde6 100644
--- a/src/HttpClientInterception/HttpClientInterceptorOptions.cs
+++ b/src/HttpClientInterception/HttpClientInterceptorOptions.cs
@@ -575,7 +575,17 @@ private static async Task BuildResponseAsync(HttpRequestMes
{
var responses = _mappings.Values
.OrderByDescending((p) => p.Priority.HasValue)
- .ThenBy((p) => p.Priority);
+ .ThenBy((p) => p.Priority)
+ .ThenByDescending((p) =>
+ {
+ // Sort by the number of request headers, so that those with more headers expected are matched first.
+ // Count() is not used to avoid side-effects of enumerating the headers if they are dynamically generated.
+#if NET8_0_OR_GREATER
+ return p.RequestHeaders?.TryGetNonEnumeratedCount(out var count) is true ? count : 0;
+#else
+ return p.RequestHeaders is ICollection>> collection ? collection.Count : 0;
+#endif
+ });
foreach (var response in responses)
{
diff --git a/tests/HttpClientInterception.Tests/Bundles/BundleExtensionsTests.cs b/tests/HttpClientInterception.Tests/Bundles/BundleExtensionsTests.cs
index 3d1aaa67..da38bfc2 100644
--- a/tests/HttpClientInterception.Tests/Bundles/BundleExtensionsTests.cs
+++ b/tests/HttpClientInterception.Tests/Bundles/BundleExtensionsTests.cs
@@ -601,4 +601,34 @@ public static void Can_Register_Bundle_With_Null_Header_Values_In_Bundle()
// Act
options.RegisterBundle(Path.Join("Bundles", "templated-bundle-null-headers.json"), headers);
}
+
+ [Fact]
+ public static async Task Can_Intercept_Http_Requests_With_Correct_Precedence_For_Http_Request_Headers()
+ {
+ // Arrange
+ var options = new HttpClientInterceptorOptions().ThrowsOnMissingRegistration();
+
+ var requestUrl = "https://registry.hub.docker.com/v2/user/image/manifests/latest";
+
+ var unauthorized = new Dictionary()
+ {
+ ["Accept"] = "application/vnd.oci.image.index.v1+json",
+ };
+
+ var authorized = new Dictionary()
+ {
+ ["Accept"] = "application/vnd.oci.image.index.v1+json",
+ ["Authorization"] = "Bearer not-a-real-docker-hub-token",
+ };
+
+ options.RegisterBundle(Path.Join("Bundles", "header-matching.json"));
+
+ // Act
+ var first = await HttpAssert.GetAsync(options, requestUrl, headers: unauthorized);
+ var second = await HttpAssert.GetAsync(options, requestUrl, headers: authorized);
+
+ // Assert
+ first.ShouldBe("unauthorized");
+ second.ShouldBe("authorized");
+ }
}
diff --git a/tests/HttpClientInterception.Tests/Bundles/header-matching.json b/tests/HttpClientInterception.Tests/Bundles/header-matching.json
new file mode 100644
index 00000000..2eab7114
--- /dev/null
+++ b/tests/HttpClientInterception.Tests/Bundles/header-matching.json
@@ -0,0 +1,36 @@
+{
+ "$schema": "https://raw.githubusercontent.com/justeattakeaway/httpclient-interception/main/src/HttpClientInterception/Bundles/http-request-bundle-schema.json",
+ "id": "test-http-request-bundle",
+ "comment": "An HTTP request bundle for testing HTTP request header matching for the same URL.",
+ "version": 1,
+ "items": [
+ {
+ "id": "authorized",
+ "comment": "An HTTP request with an Authorization header.",
+ "uri": "https://registry.hub.docker.com/v2/user/image/manifests/latest",
+ "requestHeaders": {
+ "Accept": [
+ "application/vnd.oci.image.index.v1+json"
+ ],
+ "Authorization": [
+ "Bearer not-a-real-docker-hub-token"
+ ]
+ },
+ "contentString": "authorized"
+ },
+ {
+ "id": "unauthorized",
+ "comment": "An HTTP request without an Authorization header.",
+ "uri": "https://registry.hub.docker.com/v2/user/image/manifests/latest",
+ "requestHeaders": {
+ "Accept": [
+ "application/vnd.oci.image.index.v1+json"
+ ]
+ },
+ "contentHeaders": {
+ "Content-Type": [ "application/json" ]
+ },
+ "contentString": "unauthorized"
+ }
+ ]
+}