reverse-proxy icon indicating copy to clipboard operation
reverse-proxy copied to clipboard

YarpOutputCachePolicyProvider breaks AOT compilation in 2.2.0

Open DavidZidar opened this issue 1 year ago • 2 comments

Describe the bug

I just updated to 2.2.0 and while validating my proxy I get an exception during startup caused by YarpOutputCachePolicyProvider, probably because it is using reflection and it's trying access code that is trimmed away. I'm not even using output caching.

Here's the relevant part of the stack trace:

Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Reflection.DynamicInvokeInfo..ctor(MethodBase, IntPtr) + 0x314
   at Internal.Reflection.Execution.ExecutionEnvironmentImplementation.TryGetMethodInvokeInfo(RuntimeTypeHandle, QMethodDefinition, RuntimeTypeHandle[], MethodBase, MethodSignatureComparer&, CanonicalFormKind) + 0x191
   at Internal.Reflection.Execution.ExecutionEnvironmentImplementation.TryGetMethodInvoker(RuntimeTypeHandle, QMethodDefinition, RuntimeTypeHandle[]) + 0xe3
   at Internal.Reflection.Core.Execution.ExecutionEnvironment.GetMethodInvoker(RuntimeTypeInfo, QMethodDefinition, RuntimeTypeInfo[], MemberInfo, Exception&) + 0x11e
   at System.Reflection.Runtime.MethodInfos.NativeFormat.NativeFormatMethodCommon.GetUncachedMethodInvoker(RuntimeTypeInfo[], MemberInfo, Exception&) + 0x50
   at System.Reflection.Runtime.MethodInfos.RuntimeNamedMethodInfo`1.GetUncachedMethodInvoker(RuntimeTypeInfo[], MemberInfo) + 0x1b
   at System.Reflection.Runtime.PropertyInfos.RuntimePropertyInfo.GetValue(Object, BindingFlags, Binder, Object[], CultureInfo) + 0x54
   at System.Reflection.PropertyInfo.GetValue(Object, Object[]) + 0x1d
   at Yarp.ReverseProxy.Configuration.YarpOutputCachePolicyProvider..ctor(IOptions`1 outputCacheOptions) + 0xa7

Here is what I think the offending code is: https://github.com/microsoft/reverse-proxy/blob/99ce21a64e2115f9887e99025ebd2cc2a098d656/src/ReverseProxy/Configuration/IYarpOutputCachePolicyProvider.cs#L35-L37

To Reproduce

Publish using PublishAot and attempt to run the application.

Potential fix

You might be able to use the UnsafeAccessorAttribute in .NET 8:

// Define an unsafe accessor
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_NamedPolicies")]
private static extern Dictionary<string, IOutputCachePolicy>? GetNamedPolicies(OutputCacheOptions c);

// Then use it like this
_policyMap = GetNamedPolicies(_outputCacheOptions);

For older versions of .NET you need to make sure that the code you are trying to use isn't being trimmed, one way I've successfully done this in my own projects is to create an unused field that references the type with the DynamicallyAccessedMembersAttribute stating what I need to keep.

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
private static readonly Type keepOutputCacheOptions = typeof(OutputCacheOptions);

Further technical details

  • Version 2.2.0
  • Windows

DavidZidar avatar Sep 08 '24 11:09 DavidZidar

I tried to get my potential fixes above to work but had no luck, I think more code is being trimmed than I first suspected.

However, I was able to figure out that adding the following line is enough to make the compiler keep the necessary code:

new OutputCacheOptions().AddPolicy("AOT workaround", (IOutputCachePolicy)null!);

DavidZidar avatar Sep 08 '24 12:09 DavidZidar

Thanks for raising the issue. This looks like a Native AOT bug and appears to be fixed in .NET 9. I think we should add a workaround in YARP to unblock .NET 8 though.

MihaZupan avatar Sep 11 '24 14:09 MihaZupan

I can confirm that there is no need for a workaround in .NET 9, I updated my proxy yesterday and it's working perfectly.

DavidZidar avatar Nov 26 '24 08:11 DavidZidar