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

System.UriFormatException: Invalid URI: The hostname could not be parsed. #479 #480

Closed
junalmeida opened this issue May 4, 2022 · 20 comments

Comments

@junalmeida
Copy link

junalmeida commented May 4, 2022

Describe the bug
Facing this exception while testing with dotnet test on ubuntu-latest on Azure DevOps:
I still have no idea of the value that DefaultAssemblyDirectoryFormatter is passing to Uri from the Assembly, no idea which Assembly in this the loop.

The assembly directory depends on what framework you're using, among other things, like whether you bundled the entrypoint project into a single DLL solution or not. This isn't exactly a RazorLight question, though.

The project is not bundle into a single DLL. This is happening during test on ubuntu. I'm trying to debug to see what Assembly is causing the error, but anyway simply passing Assembly.Location over to UriBuilder is known to cause problems. Don't we need a check?

To Reproduce
Steps to reproduce the behavior:
Just run tests on a setup with Embedded Resource Project.

Expected behavior
Probably expect that you might get an assembly with no valid location, as according to dotnet/corert#5467, the location may or may not exist, or could even be null.

Information (please complete the following information):

  • OS: ubuntu-lastest (from azure devops)
  • Platform: .NET 6.0
  • RazorLight version: stable 2.0.0
  • Are you using the OFFICIAL RazorLight package? Official
  • Visual Studio version [ e.g Visual Studio Community 17.8.5 ] Azure Devops, .NET 6.0 sdk / MSTEST

Additional context
It is a good practice to discuss before simply closing an issue. Thanks for understanding.

string location = assembly.Location;
UriBuilder uri = new UriBuilder(location);

System.UriFormatException: Invalid URI: The hostname could not be parsed.
  Stack Trace:
      at System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind, UriCreationOptions& creationOptions)
   at System.Uri..ctor(String uriString)
   at System.UriBuilder..ctor(String uri)
   at RazorLight.Compilation.DefaultAssemblyDirectoryFormatter.GetAssemblyDirectory(Assembly assembly)
   at RazorLight.Compilation.DefaultMetadataReferenceManager.<Resolve>b__12_2(Assembly p)
   at System.Linq.Enumerable.SelectArrayIterator`2.ToList()
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at RazorLight.Compilation.DefaultMetadataReferenceManager.Resolve(Assembly assembly, DependencyContext dependencyContext)
   at RazorLight.Compilation.DefaultMetadataReferenceManager.Resolve(Assembly assembly)
   at RazorLight.Compilation.RoslynCompilationService.EnsureOptions()
   at RazorLight.Compilation.RoslynCompilationService.get_ParseOptions()
   at RazorLight.Compilation.RoslynCompilationService.CreateSyntaxTree(SourceText sourceText)
   at RazorLight.Compilation.RoslynCompilationService.CreateCompilation(String compilationContent, String assemblyName)
   at RazorLight.Compilation.RoslynCompilationService.CompileAndEmit(IGeneratedRazorTemplate razorTemplate)
   at RazorLight.Compilation.RazorTemplateCompiler.CompileAndEmitAsync(RazorLightProjectItem projectItem)
   at RazorLight.Compilation.RazorTemplateCompiler.OnCacheMissAsync(String templateKey)
   at RazorLight.EngineHandler.CompileTemplateAsync(String key)
@junalmeida
Copy link
Author

junalmeida commented May 4, 2022

This is what I could find about the issue, I'm debugging with justMyCode = false to see which assembly was in the variable when the exception happens:

image

It seems the path is valid though.

Let me know if I can provide any other information.

@SerhiyBalan
Copy link

same issue
works perfectly at Azure Functions (Windows)
Doesn't work at all at Azure App Service (Linux)

@SerhiyBalan
Copy link

So far, the only solution I've found is to roll back to version 2.0.0-rc.3. It works well on Linux.

@jzabroski
Copy link
Collaborator

I think it's a .NET 6.0 breaking change due to the code relying on an undocumented bug in UriBuilder for unknown URI Schems.

dotnet/runtime#61363 (comment)

You were relying on an undocumented bug affecting unknown Uri schemes.

I wonder if we explicitly use the file:// protocol if it will work correctly.

@jzabroski
Copy link
Collaborator

@junalmeida Do you want to try submitting a PR to fix it if I'm right?

@SerhiyBalan
Copy link

I think it's a .NET 6.0 breaking change due to the code relying on an undocumented bug in UriBuilder for unknown URI Schems.

my backend uses Core 3.1 and runs under Linux, and I have exactly the same issue

when I run it locally under Windows, everything works well

@jzabroski
Copy link
Collaborator

@SerhiyBalan

  1. The tests run on all 3 major OSes: /~https://github.com/toddams/RazorLight/actions/runs/2071823518
  2. The tests run on many different .NET Core Onward TFMs: /~https://github.com/toddams/RazorLight/blob/master/tests/RazorLight.Tests/RazorLight.Tests.csproj#L4

And then there are tests that effectively exercise this general code path concept, indirectly:

private readonly string _contentRootPath = PathUtility.GetViewsPath();
(It probably should directly use the production code, but whatever).

@SerhiyBalan
Copy link

SerhiyBalan commented May 6, 2022

@jzabroski
2.0.0-rc.3 works perfectly for me

2.0.0-rc.4 and later - doesn't work - System.UriFormatException: Invalid URI: The hostname could not be parsed. Stack trace refers to the same file as OP mentioned

So currently I'm staying on the outdated version because it works at the production

@jzabroski
Copy link
Collaborator

I see that I added #407 which uses AppContext.BaseDirectory for unit tests.

If you add the following and replace the DI reference for the interface, does it work?

	public class PassthroughAssemblyDirectoryFormatter : IAssemblyDirectoryFormatter
	{
		public string GetAssemblyDirectory(Assembly assembly)
		{
			return assembly.Location;
		}
	}

@junalmeida
Copy link
Author

@jzabroski I would love to have the necessary expertise on your project to issue a good PR to fix this, however I don't think I have it yet, i.e I don't know how are your practices on tests and stuff.

I have one more thing to add:

I'm currently NOT using DI, instead, I'm using the RazorLightEngineBuilder which doesn't have the UseNetFrameworkLegacyFix. So to make a test I temporarily changed it to a self-contained ServiceCollection like this:

var services = new ServiceCollection();
        services.AddRazorLight()
            .UseNetFrameworkLegacyFix()
            .UseMemoryCachingProvider()
            .SetOperatingAssembly(typeof(HtmlGenerator).Assembly)
            .ExcludeAssemblies(ExcludedAssemblies);

        var project = new RazorLight.Razor.EmbeddedRazorProject(typeof(HtmlGenerator).Assembly, "REDACTED.HtmlTemplates")
        {
            Extension = "",
        };

        services.AddSingleton<RazorLight.Razor.RazorLightProject>(project);

        var container = services.BuildServiceProvider();
        engine = (RazorLightEngine)container.GetRequiredService<IRazorLightEngine>();

On the other end, the AddRazorLight doesn't have UseProject which I am using, so as a side note, those two ways to build the engine are not consistent between each other.

I can confirm that UseNetFrameworkLegacyFix works for me on version 2.0.0 on linux.

@jzabroski
Copy link
Collaborator

@junalmeida I see. The reason the UseNetFrameworkLegacyFix is called that is because Assembly.CodeBase is undefined in a single file application and now throws an exception in .NET Core. See: https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file/overview#api-incompatibility

Technically, Assembly.CodeBase still works when you're not in a single file application scenario. However, it is brittle. I suppose that:

  • UseNetFrameworkLegacyFix() could be renamed to UseAssemblyCodeBaseDirectoryFormatter()
  • LegacyFixAssemblyDirectoryFormatter could be renamed AssemblyCodeBaseDirectoryFormatter

@SerhiyBalan Does that work for you? I still think we don't have an explanation for why you can't use UriBuilder - can you try a simple console app that calls the new UriBuilder constructor:

public void Main()
{
  var location = typeof(Program).Assembly.Location();
  Console.WriteLine(location);
  var uri = new UriBuilder(location);
}

@junalmeida

This comment was marked as abuse.

@SerhiyBalan
Copy link

@jzabroski Thank you for helping!

I'm on vacation and will return on Tuesday. I will try everything from here and let you know if anything helped me to resolve the issue with the latest version of RazorLight

@jzabroski
Copy link
Collaborator

@jzabroski Don't you think that would require the dev to understand the inner workings of System.Reflection?

  1. There's no reflection happening here. This is just the rules for the .NET Platform.
  2. I expect developers to understand the .NET Platform, or they will likely have disappointing careers or drive their coworkers nuts, or both.
  3. I expect developers to read and follow the manual for the products they choose to use/build on top of, even though many do not. For example, you did not follow the issue template originally, my expectations were violated, so I closed the issue.

@junalmeida

This comment was marked as abuse.

@junalmeida
Copy link
Author

Getting back to the point, I just realized that Assembly.CodeBase is deprecated:

        [Obsolete("Assembly.CodeBase and Assembly.EscapedCodeBase are only included for .NET Framework compatibility. Use Assembly.Location.", DiagnosticId = "SYSLIB0012", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
        [RequiresAssemblyFiles("This member throws an exception for assemblies embedded in a single-file app")]
        public virtual string? CodeBase { get; }

So maybe the best is to not use UriBuilder at all, since the Assembly.Location property is supposed to be platform-dependant path? I don't know what kind of values you've got on other platforms to make use of UriBuilder for file paths.

@jzabroski
Copy link
Collaborator

Most developers won't go that far IMO, only the most seniors. I've worked with all range of seniority in terms of .NET platform. I'm thinking that you might deal with a long range of seniority as well on your user base.

I can't set a standard of incompetence, because then that's what I'll get.

People who waste my time by not following adult etiquette, like reading documentation and generally understanding their job, are an unfortunate part of participating in open source software.

I mean, the dev would need to know what the property CodeBase means to fully understand what a method named after UseAssemblyCodeBase does.

Correct.

My expectations as a user were also violated as the issue was closed with no chance of editing and improving it, we are even! 👯

This is a warning - you are trolling this thread at this point.

@junalmeida

This comment was marked as abuse.

@SerhiyBalan
Copy link

SerhiyBalan commented May 10, 2022

@jzabroski

Code in use:

public class RazorEngineService : IHtmlTemplateRendererService
{
	private static readonly IRazorLightEngine EngineService;

	static RazorEngineService()
	{
		EngineService = new RazorLightEngineBuilder()
			.UseEmbeddedResourcesProject(typeof(RazorEngineService))
			.SetOperatingAssembly(typeof(RazorEngineService).Assembly)
			.UseMemoryCachingProvider()
			.Build();
	}
	
	public async Task<string> GenerateCodeAsync<T>(string path, T model, string key)
	{
		var cacheResult = EngineService.Handler.Cache.RetrieveTemplate(key);
		if (cacheResult.Success)
		{
			// report is already compiled, render using a cache
			var templatePage = cacheResult.Template.TemplatePageFactory();
			return await EngineService.RenderTemplateAsync(templatePage, model);
		}

		return await EngineService.CompileRenderAsync(path, model);
	}
}

...

services.AddScoped<IHtmlTemplateRendererService, RazorEngineService>();

Error stack trace (Azure, Linux, Core 3.1)

at System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind) at System.Uri..ctor(String uriString) at System.UriBuilder..ctor(String uri) at RazorLight.Compilation.DefaultAssemblyDirectoryFormatter.GetAssemblyDirectory(Assembly assembly) at RazorLight.Compilation.DefaultMetadataReferenceManager.b__12_2(Assembly p) at System.Linq.Enumerable.SelectArrayIterator2.ToList() at System.Linq.Enumerable.ToList[TSource](IEnumerable1 source) at RazorLight.Compilation.DefaultMetadataReferenceManager.Resolve(Assembly assembly, DependencyContext dependencyContext) at RazorLight.Compilation.DefaultMetadataReferenceManager.Resolve(Assembly assembly) at RazorLight.Compilation.RoslynCompilationService.EnsureOptions() at RazorLight.Compilation.RoslynCompilationService.get_ParseOptions() at RazorLight.Compilation.RoslynCompilationService.CreateSyntaxTree(SourceText sourceText) at RazorLight.Compilation.RoslynCompilationService.CreateCompilation(String compilationContent, String assemblyName) at RazorLight.Compilation.RoslynCompilationService.CompileAndEmit(IGeneratedRazorTemplate razorTemplate) at RazorLight.Compilation.RazorTemplateCompiler.CompileAndEmitAsync(RazorLightProjectItem projectItem) at RazorLight.Compilation.RazorTemplateCompiler.OnCacheMissAsync(String templateKey) at RazorLight.Compilation.RazorTemplateCompiler.OnCacheMissAsync(String templateKey) at RazorLight.EngineHandler.CompileTemplateAsync(String key) at RazorLight.EngineHandler.CompileRenderAsync[T](String key, T model, ExpandoObject viewBag) at BackupRadar.Templates.RazorEngineService.GenerateCodeAsync[T](String path, T model, String key) in ....\RazorEngineService.cs

The suggestion to use PassthroughAssemblyDirectoryFormatter didn't work for me
I used the following code at application startup to replace:

        public class PassthroughAssemblyDirectoryFormatter : IAssemblyDirectoryFormatter
        {
            public string GetAssemblyDirectory(Assembly assembly)
            {
                return assembly.Location;
            }
        }

.... 
        services.Replace(ServiceDescriptor.Transient<IAssemblyDirectoryFormatter, PassthroughAssemblyDirectoryFormatter>());`

If you have some suggestions, please go ahead

@jzabroski
Copy link
Collaborator

jzabroski commented May 10, 2022

@SerhiyBalan Can I get detailed info on your Linux environment, e.g., which linux distro and version and libc and glibc? Maybe best to open a new ticket. This is great - I should be able to figure out a better approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants