diff --git a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs
index 0387bb131057..f996a75b7168 100644
--- a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs
+++ b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs
@@ -116,6 +116,8 @@ public override bool Execute()
endpoint.AssetFile = asset.ResolvedAsset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance);
endpoint.Route = route;
+ EncodeLinkHeadersIfNeeded(endpoint);
+
Log.LogMessage(MessageImportance.Low, "Including endpoint '{0}' for asset '{1}' with final location '{2}'", endpoint.Route, endpoint.AssetFile, asset.TargetPath);
}
@@ -137,6 +139,48 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}
+ private static void EncodeLinkHeadersIfNeeded(StaticWebAssetEndpoint endpoint)
+ {
+ for (var i = 0; i < endpoint.ResponseHeaders.Length; i++)
+ {
+ ref var header = ref endpoint.ResponseHeaders[i];
+ if (!string.Equals(header.Name, "Link", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ var headerValues = header.Value.Split([','], StringSplitOptions.RemoveEmptyEntries);
+ for (var j = 0; j < headerValues.Length; j++)
+ {
+ ref var value = ref headerValues[j];
+ value = EncodeHeaderValue(value);
+ }
+ header.Value = string.Join(",", headerValues);
+ }
+ }
+
+ private static string EncodeHeaderValue(string header)
+ {
+ var index = header.IndexOf('<');
+ if (index == -1)
+ {
+ return header;
+ }
+ index++;
+ var endIndex = header.IndexOf('>', index);
+ if (endIndex == -1)
+ {
+ return header;
+ }
+ var link = header.AsSpan(index, endIndex - index).ToString();
+ var segments = link.Split('/');
+ for (var j = 0; j < segments.Length; j++)
+ {
+ segments[j] = System.Net.WebUtility.UrlEncode(segments[j]);
+ }
+ var encoded = string.Join("/", segments);
+ return $"{header.Substring(0, index)}{encoded}{header.Substring(endIndex)}";
+ }
+
private static (string, string[]) ParseAndSortPatterns(string patterns)
{
if (string.IsNullOrEmpty(patterns))
diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs
index 9668d8656913..6155a3498987 100644
--- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs
+++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.StaticWebAssets.Tasks;
@@ -617,5 +618,72 @@ public void RegeneratingScopedCss_ForProjectWithReferences()
text.Should().Contain("background-color: orangered");
text.Should().MatchRegex(""".*@import '_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*""");
}
+
+ [Fact]
+ public void Build_GeneratesUrlEncodedLinkHeaderForNonAsciiProjectName()
+ {
+ var testAsset = "RazorAppWithPackageAndP2PReference";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ // Rename the ClassLibrary project to have non-ASCII characters
+ var originalLibPath = Path.Combine(ProjectDirectory.Path, "AnotherClassLib");
+ var newLibPath = Path.Combine(ProjectDirectory.Path, "项目");
+ Directory.Move(originalLibPath, newLibPath);
+
+ // Update the project file to set the assembly name and package ID
+ var libProjectFile = Path.Combine(newLibPath, "AnotherClassLib.csproj");
+ var newLibProjectFile = Path.Combine(newLibPath, "项目.csproj");
+ File.Move(libProjectFile, newLibProjectFile);
+
+ // Add assembly name property to ensure consistent naming
+ var libProjectContent = File.ReadAllText(newLibProjectFile);
+ // Find the first PropertyGroup closing tag and replace it
+ var targetPattern = "";
+ var replacement = " 项目\n 项目\n ";
+ var index = libProjectContent.IndexOf(targetPattern);
+ if (index >= 0)
+ {
+ libProjectContent = libProjectContent.Substring(0, index) + replacement + libProjectContent.Substring(index + targetPattern.Length);
+ }
+ File.WriteAllText(newLibProjectFile, libProjectContent);
+
+ // Update the main project to reference the renamed library
+ var mainProjectFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "AppWithPackageAndP2PReference.csproj");
+ var mainProjectContent = File.ReadAllText(mainProjectFile);
+ mainProjectContent = mainProjectContent.Replace(@"..\AnotherClassLib\AnotherClassLib.csproj", @"..\项目\项目.csproj");
+ File.WriteAllText(mainProjectFile, mainProjectContent);
+
+ // Ensure library has scoped CSS
+ var libCssFile = Path.Combine(newLibPath, "Views", "Shared", "Index.cshtml.css");
+ if (!File.Exists(libCssFile))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(libCssFile));
+ File.WriteAllText(libCssFile, ".test { color: red; }");
+ }
+
+ EnsureLocalPackagesExists();
+
+ var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
+ ExecuteCommand(restore).Should().Pass();
+
+ var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
+ ExecuteCommand(build).Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ // Check that the staticwebassets.build.endpoints.json file contains URL-encoded characters
+ var endpointsFile = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json");
+ new FileInfo(endpointsFile).Should().Exist();
+
+ var endpointsContent = File.ReadAllText(endpointsFile);
+ var json = JsonSerializer.Deserialize(endpointsContent, new JsonSerializerOptions(JsonSerializerDefaults.Web));
+
+ var styles = json.Endpoints.Where(e => e.Route.EndsWith("styles.css"));
+
+ foreach (var styleEndpoint in styles)
+ {
+ styleEndpoint.ResponseHeaders.Should().Contain(h => h.Name.Equals("Link", StringComparison.OrdinalIgnoreCase) && h.Value.Contains("%E9%A1%B9%E7%9B%AE"));
+ }
+ }
}
}