From 1dbc604b0a84fee378eb6d4aa9a311a6d61f9137 Mon Sep 17 00:00:00 2001 From: Jonathan Souza Date: Mon, 22 Jun 2026 12:22:46 +1200 Subject: [PATCH] Fix ReflectionTypeLoadException for ILambdaResponseStream when bundling older Amazon.Lambda.Core Replace direct ILambdaResponseStream interface implementation (ImplLambdaResponseStream) with DispatchProxy-based dynamic implementation. This prevents GetTypes() on the Amazon.Lambda.RuntimeSupport assembly from triggering TypeLoadException when customers bundle Amazon.Lambda.Core < 3.0.0 (which lacks the interface type). The DispatchProxy approach generates the interface implementation at runtime only when the loaded Amazon.Lambda.Core has the ResponseStreaming types. If the types are absent, InitializeCore() returns gracefully without error. Fixes #2430 --- .../5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json | 11 ++ ...onseStreamLambdaCoreInitializerIsolated.cs | 105 +++++++++++++----- .../LambdaResponseStreamingCoreTests.cs | 33 +++--- .../TestImplLambdaResponseStream.cs | 26 +++++ 4 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 .autover/changes/5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestImplLambdaResponseStream.cs diff --git a/.autover/changes/5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json b/.autover/changes/5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json new file mode 100644 index 000000000..4a85a6c5a --- /dev/null +++ b/.autover/changes/5d462e9b-d94a-4005-9b7c-ddc43b5ab0d9.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Patch", + "ChangelogMessages": [ + "Fixed ReflectionTypeLoadException when customer bundles Amazon.Lambda.Core \u003C 3.0.0 and runtime scans types. Replaced direct ILambdaResponseStream implementation with DispatchProxy to avoid exposing the type in assembly metadata." + ] + } + ] +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs index 2cb46e3ce..fff3d06e5 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs @@ -2,58 +2,109 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Amazon.Lambda.Core.ResponseStreaming; using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; #pragma warning disable CA2252 namespace Amazon.Lambda.RuntimeSupport { /// - /// This class is used to connect the created by to Amazon.Lambda.Core with it's public interfaces. - /// The deployed Lambda function might be referencing an older version of Amazon.Lambda.Core that does not have the public interfaces for response streaming, - /// so this class is used to avoid a direct dependency on Amazon.Lambda.Core in the rest of the response streaming implementation. + /// This class connects the created by + /// to Amazon.Lambda.Core's public response streaming interfaces. /// - /// Any code referencing this class must wrap the code around a try/catch for to allow for the case where the Lambda function - /// is deployed with an older version of Amazon.Lambda.Core that does not have the response streaming interfaces. + /// The deployed Lambda function might reference an older Amazon.Lambda.Core that does not have + /// the response streaming interfaces. To prevent ReflectionTypeLoadException when customer code + /// calls GetTypes() on this assembly, NO type in this class (or this assembly) directly implements + /// ILambdaResponseStream. Instead, we use DispatchProxy to generate the implementation dynamically + /// at runtime, only when the interface type is confirmed to exist in the loaded Amazon.Lambda.Core. + /// See: https://github.com/aws/aws-lambda-dotnet/issues/2430 /// /// internal class ResponseStreamLambdaCoreInitializerIsolated { /// - /// Initalize Amazon.Lambda.Core with a factory method for creating that wraps the internal implementation. + /// Initialize Amazon.Lambda.Core with a factory method for creating response streams. + /// All type references to ILambdaResponseStream are made via reflection to avoid embedding + /// the type dependency in this assembly's metadata. /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Response streaming types are preserved by the runtime and only loaded when available")] + [UnconditionalSuppressMessage("Trimming", "IL2055", Justification = "Response streaming types are preserved by the runtime")] + [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "Response streaming types are preserved by the runtime")] + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Response streaming types are preserved by the runtime")] + [UnconditionalSuppressMessage("Trimming", "IL2080", Justification = "Response streaming types are preserved by the runtime")] internal static void InitializeCore() { -#if !ANALYZER_UNIT_TESTS // This precompiler directive is used to avoid the unit tests from needing a dependency on Amazon.Lambda.Core. - Func factory = (byte[] prelude) => new ImplLambdaResponseStream(ResponseStreamFactory.CreateStream(prelude)); - LambdaResponseStreamFactory.SetLambdaResponseStream(factory); +#if !ANALYZER_UNIT_TESTS + var coreAssembly = typeof(Amazon.Lambda.Core.ILambdaContext).Assembly; + + // Check if the loaded Amazon.Lambda.Core has the response streaming types. + // If not (older version loaded from /var/task), bail out gracefully — no exception. + var interfaceType = coreAssembly.GetType("Amazon.Lambda.Core.ResponseStreaming.ILambdaResponseStream"); + if (interfaceType == null) return; + + var factoryType = coreAssembly.GetType("Amazon.Lambda.Core.ResponseStreaming.LambdaResponseStreamFactory"); + if (factoryType == null) return; + + var setMethod = factoryType.GetMethod("SetLambdaResponseStream", BindingFlags.NonPublic | BindingFlags.Static); + if (setMethod == null) return; + + // Create a Func using DispatchProxy. + // ResponseStreamProxy generates a runtime type implementing T (ILambdaResponseStream) + // that forwards calls to a ResponseStream instance. + var proxyFactoryMethod = typeof(ResponseStreamLambdaCoreInitializerIsolated) + .GetMethod(nameof(BuildFactory), BindingFlags.NonPublic | BindingFlags.Static) + .MakeGenericMethod(interfaceType); + + var factory = proxyFactoryMethod.Invoke(null, null); + setMethod.Invoke(null, new object[] { factory }); #endif } - /// - /// Implements the interface by wrapping a . This is used to connect the internal response streaming implementation to the public interfaces in Amazon.Lambda.Core. - /// - internal class ImplLambdaResponseStream : ILambdaResponseStream + private static Func BuildFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : class { - private readonly ResponseStream _innerStream; - - internal ImplLambdaResponseStream(ResponseStream innerStream) + return (byte[] prelude) => { - _innerStream = innerStream; - } + var stream = ResponseStreamFactory.CreateStream(prelude); + var proxy = DispatchProxy.Create(); + ((ResponseStreamProxy)(object)proxy).SetInner(stream); + return proxy; + }; + } + } - /// - public long BytesWritten => _innerStream.BytesWritten; + /// + /// A DispatchProxy that forwards ILambdaResponseStream calls to a ResponseStream instance. + /// This class does NOT implement ILambdaResponseStream at compile time — DispatchProxy generates + /// the interface implementation dynamically at runtime, avoiding the ReflectionTypeLoadException. + /// + internal class ResponseStreamProxy : DispatchProxy + { + private ResponseStream _inner; - /// - public bool HasError => _innerStream.HasError; + internal void SetInner(ResponseStream inner) + { + _inner = inner; + } - /// - public void Dispose() => _innerStream.Dispose(); + protected override object Invoke(MethodInfo targetMethod, object[] args) + { + return targetMethod.Name switch + { + "WriteAsync" => _inner.WriteAsync((byte[])args[0], (int)args[1], (int)args[2], + args.Length > 3 ? (CancellationToken)args[3] : default), + "Dispose" => DoDispose(), + "get_BytesWritten" => _inner.BytesWritten, + "get_HasError" => _inner.HasError, + _ => null + }; + } - /// - public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) => _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + private object DoDispose() + { + _inner.Dispose(); + return null; } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs index 5da3d5e8b..3908020f9 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs @@ -1,3 +1,4 @@ +using Amazon.Lambda.RuntimeSupport.UnitTests; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 #pragma warning disable CA2252 @@ -167,7 +168,7 @@ public class LambdaResponseStreamTests var output = new MemoryStream(); await inner.SetHttpOutputStreamAsync(output); - var implStream = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var implStream = new TestImplLambdaResponseStream(inner); var lambdaStream = new LambdaResponseStream(implStream); return (lambdaStream, output); } @@ -176,7 +177,7 @@ public class LambdaResponseStreamTests public void LambdaResponseStream_IsStreamSubclass() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.IsAssignableFrom(stream); @@ -186,7 +187,7 @@ public void LambdaResponseStream_IsStreamSubclass() public void CanWrite_IsTrue() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.True(stream.CanWrite); @@ -196,7 +197,7 @@ public void CanWrite_IsTrue() public void CanRead_IsFalse() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.False(stream.CanRead); @@ -206,7 +207,7 @@ public void CanRead_IsFalse() public void CanSeek_IsFalse() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.False(stream.CanSeek); @@ -216,7 +217,7 @@ public void CanSeek_IsFalse() public void Read_ThrowsNotImplementedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => stream.Read(new byte[1], 0, 1)); @@ -226,7 +227,7 @@ public void Read_ThrowsNotImplementedException() public void ReadAsync_ThrowsNotImplementedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); // ReadAsync throws synchronously (not async) — capture the thrown task @@ -239,7 +240,7 @@ public void ReadAsync_ThrowsNotImplementedException() public void Seek_ThrowsNotImplementedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); @@ -249,7 +250,7 @@ public void Seek_ThrowsNotImplementedException() public void Position_Get_ThrowsNotSupportedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => _ = stream.Position); @@ -259,7 +260,7 @@ public void Position_Get_ThrowsNotSupportedException() public void Position_Set_ThrowsNotSupportedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => stream.Position = 0); @@ -269,7 +270,7 @@ public void Position_Set_ThrowsNotSupportedException() public void SetLength_ThrowsNotSupportedException() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var stream = new LambdaResponseStream(impl); Assert.Throws(() => stream.SetLength(100)); @@ -342,7 +343,7 @@ public async Task WriteAsync_DelegatesToInnerResponseStream() var output = new MemoryStream(); await inner.SetHttpOutputStreamAsync(output); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); var data = new byte[] { 1, 2, 3 }; await impl.WriteAsync(data, 0, data.Length); @@ -357,7 +358,7 @@ public async Task BytesWritten_ReflectsInnerStreamBytesWritten() var output = new MemoryStream(); await inner.SetHttpOutputStreamAsync(output); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); await impl.WriteAsync(new byte[7], 0, 7); Assert.Equal(7, impl.BytesWritten); @@ -367,7 +368,7 @@ public async Task BytesWritten_ReflectsInnerStreamBytesWritten() public void HasError_InitiallyFalse() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); Assert.False(impl.HasError); } @@ -378,7 +379,7 @@ public void HasError_TrueAfterReportError() var inner = new ResponseStream(Array.Empty()); inner.ReportError(new Exception("test")); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); Assert.True(impl.HasError); } @@ -387,7 +388,7 @@ public void HasError_TrueAfterReportError() public void Dispose_DisposesInnerStream() { var inner = new ResponseStream(Array.Empty()); - var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var impl = new TestImplLambdaResponseStream(inner); // Should not throw impl.Dispose(); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestImplLambdaResponseStream.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestImplLambdaResponseStream.cs new file mode 100644 index 000000000..00c736b70 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestImplLambdaResponseStream.cs @@ -0,0 +1,26 @@ +// Test-only implementation of ILambdaResponseStream for unit tests. +// In production, DispatchProxy is used instead to avoid the type reference in assembly metadata. +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.Core.ResponseStreaming; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + internal class TestImplLambdaResponseStream : ILambdaResponseStream + { + private readonly ResponseStream _innerStream; + + internal TestImplLambdaResponseStream(ResponseStream innerStream) + { + _innerStream = innerStream; + } + + public long BytesWritten => _innerStream.BytesWritten; + public bool HasError => _innerStream.HasError; + public void Dispose() => _innerStream.Dispose(); + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + => _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } +}