From 1b4d82adba9caa63f810d577cb2531e2bdc384f9 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Tue, 7 Jul 2026 18:13:23 -0700 Subject: [PATCH 01/11] Remove RequiresPreviewFeatures attribute for Lambda Response Streaming now that the changes have been deployed to the managed runtime. --- .../9e75413c-9cb6-4521-aaca-09b557fff2b3.json | 49 +++++++++++++++++ .../HostingOptions.cs | 1 - .../APIGatewayHttpApiV2ProxyFunction.cs | 1 - .../APIGatewayProxyFunction.cs | 1 - .../AbstractAspNetCoreFunction.cs | 10 ---- .../ApplicationLoadBalancerFunction.cs | 1 - .../Internal/StreamingResponseBodyFeature.cs | 1 - .../src/Amazon.Lambda.Core/LambdaLogger.cs | 1 - .../HttpResponseStreamPrelude.cs | 1 - .../ResponseStreaming/LambdaResponseStream.cs | 1 - .../LambdaResponseStreamFactory.cs | 7 --- .../ResponseStreamingHostingTests.cs | 1 - .../ResponseStreamingPropertyTests.cs | 1 - .../BuildStreamingPreludeTests.cs | 55 ++++++++++--------- .../ResponseStreamingPropertyTests.cs | 24 ++++---- .../StreamingFunctionHandlerAsyncTests.cs | 15 ----- .../StreamingResponseBodyFeatureTests.cs | 1 - 17 files changed, 88 insertions(+), 83 deletions(-) create mode 100644 .autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json diff --git a/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json b/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json new file mode 100644 index 000000000..691797349 --- /dev/null +++ b/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json @@ -0,0 +1,49 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Core", + "Type": "Minor", + "ChangelogMessages": [ + "Lambda response streaming is now available as GA. The RequiresPreviewFeatures attribute has been removed", + "The LambdaLogger.ConfigureStructuredLogging API has been deployed to the managed runtime. The RequiresPreviewFeatures attribute has been removed" + ] + }, + { + "Name": "Amazon.Lambda.Annotations", + "Type": "Minor", + "ChangelogMessages": [ + "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support." + ] + }, + { + "Name": "Amazon.Lambda.AspNetCoreServer", + "Type": "Minor", + "ChangelogMessages": [ + "Removed RequiresPreviewFeatures attribute for Lambda Response Streaming." + "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support." + ] + }, + { + "Name": "Amazon.Lambda.AspNetCoreServer.Hosting", + "Type": "Minor", + "ChangelogMessages": [ + "Removed RequiresPreviewFeatures attribute for Lambda Response Streaming." + "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support." + ] + }, + { + "Name": "Amazon.Lambda.PowerShellHost", + "Type": "Minor", + "ChangelogMessages": [ + "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support." + ] + }, + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Minor", + "ChangelogMessages": [ + "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support." + ] + } + ] +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs index cf7ef34ba..3389368c7 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs @@ -39,7 +39,6 @@ public class HostingOptions /// FunctionHandlerAsync writes directly to the Lambda response stream and /// returns null. Requires net8.0 or later. /// - [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] public bool EnableResponseStreaming { get; set; } = false; /// diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayHttpApiV2ProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayHttpApiV2ProxyFunction.cs index 819306c30..e9578068c 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayHttpApiV2ProxyFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayHttpApiV2ProxyFunction.cs @@ -59,7 +59,6 @@ private protected override void InternalCustomResponseExceptionHandling(APIGatew /// instead of multiValueHeaders. API Gateway HTTP API v2 expects the headers /// format; using multiValueHeaders causes a 500 Internal Server Error. /// - [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] protected override Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude BuildStreamingPrelude(IHttpResponseFeature responseFeature) { var prelude = new Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs index 1398239de..4e3e7a35d 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs @@ -113,7 +113,6 @@ private protected override void InternalCustomResponseExceptionHandling(APIGatew /// /// The ASP.NET Core response feature for the current invocation. /// A populated . - [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] protected override Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude BuildStreamingPrelude(IHttpResponseFeature responseFeature) { var prelude = new Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index 51c930333..55b69f67f 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs @@ -24,12 +24,6 @@ namespace Amazon.Lambda.AspNetCoreServer /// public abstract class AbstractAspNetCoreFunction { - internal const string ParameterizedPreviewMessage = - "Response streaming is in preview till a new version of .NET Lambda runtime client that supports response streaming " + - "has been deployed to the .NET Lambda managed runtime. Till deployment has been made the feature can be used by deploying as an " + - "executable including the latest version of Amazon.Lambda.RuntimeSupport and setting the \"EnablePreviewFeatures\" in the Lambda " + - "project file to \"true\""; - /// /// Key to access the ILambdaContext object from the HttpContext.Items collection. /// @@ -206,7 +200,6 @@ public void RegisterResponseContentEncodingForContentEncoding(string contentEnco /// buffering it and returning a typed response object (which will be null). /// Requires net8.0 or later. /// - [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] public virtual bool EnableResponseStreaming { get; set; } = false; @@ -678,7 +671,6 @@ protected virtual void PostMarshallResponseFeature(IHttpResponseFeature aspNetCo /// /// The ASP.NET Core response feature for the current invocation. /// A populated . - [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] protected abstract Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude BuildStreamingPrelude(IHttpResponseFeature responseFeature); /// @@ -689,7 +681,6 @@ protected virtual void PostMarshallResponseFeature(IHttpResponseFeature aspNetCo /// /// The HTTP response prelude containing status code and headers. /// A writable for the response body. - [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] protected virtual System.IO.Stream CreateLambdaResponseStream( Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) { @@ -701,7 +692,6 @@ protected virtual System.IO.Stream CreateLambdaResponseStream( /// is true. Writes the response directly to a /// . /// - [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] private async Task ExecuteStreamingRequestAsync(InvokeFeatures features) { var responseFeature = (IHttpResponseFeature)features; diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs index 2bb08c711..6a2805088 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs @@ -225,7 +225,6 @@ private protected override void InternalCustomResponseExceptionHandling(Applicat } /// - [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] protected override HttpResponseStreamPrelude BuildStreamingPrelude(IHttpResponseFeature responseFeature) => throw new NotImplementedException(); private string GetSingleHeaderValue(ApplicationLoadBalancerRequest request, string headerName) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs index 03ec929a1..22084f4b0 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs @@ -20,7 +20,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Internal /// ; after all writes go directly to the /// obtained from the stream opener delegate. /// - [RequiresPreviewFeatures(AbstractAspNetCoreFunction.ParameterizedPreviewMessage)] internal class StreamingResponseBodyFeature : IHttpResponseBodyFeature { private readonly ILogger _logger; diff --git a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs index 38afe7371..de5ddc407 100644 --- a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs @@ -139,7 +139,6 @@ public static void Log(string level, Exception exception, string message, params /// When structured logging is enabled this method will allow overriding the default configuration the Lambda runtime uses for structured logging. /// /// The options to use for configuring structured logging. - [RequiresPreviewFeatures("This method is in preview until the latest changes of the .NET Lambda runtime client have been deployed to the Lambda managed runtimes")] public static void ConfigureStructuredLogging(StructuredLoggingOptions options) => _configureStructuredLoggingAction(options); internal static void SetConfigureStructuredLoggingAction(Action configureStructuredLoggingAction) diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs index 1a10aa2dc..4ac36491a 100644 --- a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs @@ -15,7 +15,6 @@ namespace Amazon.Lambda.Core.ResponseStreaming /// headers, and cookies for the response. The prelude must be sent as the first chunk of the response stream, followed by the response body chunks. /// This allows you to set the status code and headers for the response before sending any of the response body. /// - [RequiresPreviewFeatures(LambdaResponseStreamFactory.PreviewMessage)] public class HttpResponseStreamPrelude { /// diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs index 83ac446a4..f61bb7f3f 100644 --- a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs @@ -15,7 +15,6 @@ namespace Amazon.Lambda.Core.ResponseStreaming /// to the Lambda Runtime API. Returned by . /// Integrates with standard .NET stream consumers such as . /// - [RequiresPreviewFeatures(LambdaResponseStreamFactory.PreviewMessage)] public class LambdaResponseStream : Stream { private readonly ILambdaResponseStream _responseStream; diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs index 1b9e6d3b6..29bddfa05 100644 --- a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs @@ -10,15 +10,8 @@ namespace Amazon.Lambda.Core.ResponseStreaming /// /// Factory to create Lambda response streams for writing streaming responses in AWS Lambda functions. The created streams are write-only and non-seekable. /// - [RequiresPreviewFeatures(LambdaResponseStreamFactory.PreviewMessage)] public class LambdaResponseStreamFactory { - internal const string PreviewMessage = - "Response streaming is in preview till a new version of .NET Lambda runtime client that supports response streaming " + - "has been deployed to the .NET Lambda managed runtime. Till deployment has been made the feature can be used by deploying as an " + - "executable including the latest version of Amazon.Lambda.RuntimeSupport and setting the \"EnablePreviewFeatures\" in the Lambda " + - "project file to \"true\""; - internal const string UninitializedFactoryMessage = "LambdaResponseStreamFactory is not initialized. This is caused by mismatch versions of Amazon.Lambda.Core and Amazon.Lambda.RuntimeSupport. " + "Update both packages to the current version to address the issue."; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs index f70f91629..b9718aca9 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs @@ -16,7 +16,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; /// /// Tests for response streaming integration in hosting (Requirement 10). /// -[RequiresPreviewFeatures] public class ResponseStreamingHostingTests { [Fact] diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs index 43ebc4dd4..0717e29cd 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs @@ -14,7 +14,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; -[RequiresPreviewFeatures] public class ResponseStreamingPropertyTests { private static IServiceProvider BuildServiceProvider(HostingOptions hostingOptions) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs index c2971d0ab..2e4122b1b 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs @@ -9,7 +9,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Test { - [RequiresPreviewFeatures] public class BuildStreamingPreludeTests { // Subclass that skips host startup entirely and @@ -83,10 +82,12 @@ public void StatusCode_DefaultsTo200_WhenFeatureStatusCodeIsZero() } // ----------------------------------------------------------------------- - // 6.3 Non-Set-Cookie headers appear in MultiValueHeaders with all values preserved + // 6.3 Non-Set-Cookie headers appear in Headers with all values preserved. + // HTTP API v2 uses the single-value "headers" collection (not "multiValueHeaders"), + // so multiple values for the same header are joined with ", ". // ----------------------------------------------------------------------- [Fact] - public void NonSetCookieHeaders_AppearInMultiValueHeaders_WithAllValuesPreserved() + public void NonSetCookieHeaders_AppearInHeaders_WithAllValuesPreserved() { var builder = CreateBuilder(); var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary @@ -98,18 +99,18 @@ public void NonSetCookieHeaders_AppearInMultiValueHeaders_WithAllValuesPreserved var prelude = builder.InvokeBuildStreamingPrelude(rf); - Assert.True(prelude.MultiValueHeaders.ContainsKey("Content-Type")); - Assert.Equal(new[] { "application/json" }, prelude.MultiValueHeaders["Content-Type"]); + Assert.True(prelude.Headers.ContainsKey("Content-Type")); + Assert.Equal("application/json", prelude.Headers["Content-Type"]); - Assert.True(prelude.MultiValueHeaders.ContainsKey("X-Custom")); - Assert.Equal(new[] { "val1", "val2" }, prelude.MultiValueHeaders["X-Custom"]); + Assert.True(prelude.Headers.ContainsKey("X-Custom")); + Assert.Equal("val1, val2", prelude.Headers["X-Custom"]); - Assert.True(prelude.MultiValueHeaders.ContainsKey("Cache-Control")); - Assert.Equal(new[] { "no-cache", "no-store" }, prelude.MultiValueHeaders["Cache-Control"]); + Assert.True(prelude.Headers.ContainsKey("Cache-Control")); + Assert.Equal("no-cache, no-store", prelude.Headers["Cache-Control"]); } [Fact] - public void NonSetCookieHeaders_MultiValueHeaders_PreservesMultipleValues() + public void NonSetCookieHeaders_Headers_JoinsMultipleValues() { var builder = CreateBuilder(); var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary @@ -119,8 +120,8 @@ public void NonSetCookieHeaders_MultiValueHeaders_PreservesMultipleValues() var prelude = builder.InvokeBuildStreamingPrelude(rf); - Assert.Equal(new[] { "text/html", "application/xhtml+xml", "application/xml" }, - prelude.MultiValueHeaders["Accept"]); + Assert.Equal("text/html, application/xhtml+xml, application/xml", + prelude.Headers["Accept"]); } // ----------------------------------------------------------------------- @@ -141,12 +142,12 @@ public void SetCookieHeader_MovedToCookies_AbsentFromMultiValueHeaders() // Cookie value is in Cookies Assert.Contains("session=abc123; Path=/; HttpOnly", prelude.Cookies); - // Set-Cookie is NOT in MultiValueHeaders - Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); - Assert.False(prelude.MultiValueHeaders.ContainsKey("set-cookie")); + // Set-Cookie is NOT in Headers + Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); + Assert.False(prelude.Headers.ContainsKey("set-cookie")); // Other headers are still present - Assert.True(prelude.MultiValueHeaders.ContainsKey("Content-Type")); + Assert.True(prelude.Headers.ContainsKey("Content-Type")); } [Fact] @@ -164,8 +165,8 @@ public void SetCookieHeader_IsCaseInsensitive() var prelude = builder.InvokeBuildStreamingPrelude(rf); Assert.Contains("id=xyz; Path=/", prelude.Cookies); - Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); - Assert.False(prelude.MultiValueHeaders.ContainsKey("set-cookie")); + Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); + Assert.False(prelude.Headers.ContainsKey("set-cookie")); } // ----------------------------------------------------------------------- @@ -192,8 +193,8 @@ public void MultipleSetCookieValues_AllAppearInCookies() Assert.Contains("theme=dark; Path=/", prelude.Cookies); Assert.Contains("lang=en; Path=/; SameSite=Strict", prelude.Cookies); - // None of them should be in MultiValueHeaders - Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); + // None of them should be in Headers + Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); } [Fact] @@ -212,8 +213,8 @@ public void MultipleSetCookieValues_WithOtherHeaders_CookiesAndHeadersAreSeparat Assert.Equal(2, prelude.Cookies.Count); Assert.Contains("a=1", prelude.Cookies); Assert.Contains("b=2", prelude.Cookies); - Assert.True(prelude.MultiValueHeaders.ContainsKey("Location")); - Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); + Assert.True(prelude.Headers.ContainsKey("Location")); + Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); } [Fact] @@ -225,7 +226,7 @@ public void EmptyHeaders_ProducesEmptyMultiValueHeadersAndCookies() var prelude = builder.InvokeBuildStreamingPrelude(rf); Assert.Equal(HttpStatusCode.NoContent, prelude.StatusCode); - Assert.Empty(prelude.MultiValueHeaders); + Assert.Empty(prelude.Headers); Assert.Empty(prelude.Cookies); } @@ -244,8 +245,8 @@ public void ContentLengthHeader_ExcludedFromPrelude() var prelude = builder.InvokeBuildStreamingPrelude(rf); - Assert.True(prelude.MultiValueHeaders.ContainsKey("Content-Type")); - Assert.False(prelude.MultiValueHeaders.ContainsKey("Content-Length")); + Assert.True(prelude.Headers.ContainsKey("Content-Type")); + Assert.False(prelude.Headers.ContainsKey("Content-Length")); } [Fact] @@ -260,8 +261,8 @@ public void TransferEncodingHeader_ExcludedFromPrelude() var prelude = builder.InvokeBuildStreamingPrelude(rf); - Assert.True(prelude.MultiValueHeaders.ContainsKey("Content-Type")); - Assert.False(prelude.MultiValueHeaders.ContainsKey("Transfer-Encoding")); + Assert.True(prelude.Headers.ContainsKey("Content-Type")); + Assert.False(prelude.Headers.ContainsKey("Transfer-Encoding")); } } } diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs index 05c6bed87..013c015a3 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs @@ -20,7 +20,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Test { - [RequiresPreviewFeatures] public class ResponseStreamingPropertyTests { // ----------------------------------------------------------------------- @@ -52,7 +51,6 @@ protected override void PostMarshallItemsFeatureFeature( base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); } - [RequiresPreviewFeatures] protected override Stream CreateLambdaResponseStream( Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) { @@ -195,12 +193,13 @@ public void Property3_Prelude_StatusCodeAndNonCookieHeaders_Correct( int expectedStatus = statusCode == 0 ? 200 : statusCode; Assert.Equal((System.Net.HttpStatusCode)expectedStatus, prelude.StatusCode); - Assert.True(prelude.MultiValueHeaders.ContainsKey(headerKey), - $"Header '{headerKey}' missing from MultiValueHeaders"); - Assert.Equal(headerValues, prelude.MultiValueHeaders[headerKey].ToArray()); + // HTTP API v2 uses the single-value "headers" collection; multiple values are joined with ", ". + Assert.True(prelude.Headers.ContainsKey(headerKey), + $"Header '{headerKey}' missing from Headers"); + Assert.Equal(string.Join(", ", headerValues), prelude.Headers[headerKey]); - Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); - Assert.False(prelude.MultiValueHeaders.ContainsKey("set-cookie")); + Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); + Assert.False(prelude.Headers.ContainsKey("set-cookie")); } @@ -227,12 +226,12 @@ public void Property4_SetCookieHeaders_MovedToCookies_AbsentFromMultiValueHeader foreach (var cookie in cookies) Assert.Contains(cookie, prelude.Cookies); - Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie"), - "Set-Cookie must not appear in MultiValueHeaders"); - Assert.False(prelude.MultiValueHeaders.ContainsKey("set-cookie"), - "set-cookie must not appear in MultiValueHeaders"); + Assert.False(prelude.Headers.ContainsKey("Set-Cookie"), + "Set-Cookie must not appear in Headers"); + Assert.False(prelude.Headers.ContainsKey("set-cookie"), + "set-cookie must not appear in Headers"); - Assert.True(prelude.MultiValueHeaders.ContainsKey("content-type")); + Assert.True(prelude.Headers.ContainsKey("content-type")); } @@ -446,7 +445,6 @@ protected override void PostMarshallItemsFeatureFeature( base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); } - [RequiresPreviewFeatures] protected override Stream CreateLambdaResponseStream( Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) { diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs index 6ba2a6291..ef5c9711a 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs @@ -30,7 +30,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Test /// a instead of calling LambdaResponseStreamFactory.CreateHttpStream, /// allowing tests to run without the Lambda runtime. /// - [RequiresPreviewFeatures] public class StreamingFunctionHandlerAsyncTests { // ----------------------------------------------------------------------- @@ -75,7 +74,6 @@ protected override void PostMarshallItemsFeatureFeature( base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); } - [RequiresPreviewFeatures] protected override Stream CreateLambdaResponseStream( Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) { @@ -467,18 +465,6 @@ public void FunctionHandlerAsync_HasLambdaSerializerAttribute() attr.SerializerType); } - [Fact] - public void EnableResponseStreaming_Property_HasRequiresPreviewFeaturesAttribute() - { - var prop = typeof(APIGatewayHttpApiV2ProxyFunction) - .GetProperty(nameof(APIGatewayHttpApiV2ProxyFunction.EnableResponseStreaming)); - - Assert.NotNull(prop); - - var attr = prop.GetCustomAttribute(); - Assert.NotNull(attr); - } - [Fact] public void EnableResponseStreaming_Property_DefaultsToFalse() { @@ -531,7 +517,6 @@ protected CustomPipelineStreamingFunction() EnableResponseStreaming = true; } - [RequiresPreviewFeatures] protected override Stream CreateLambdaResponseStream( Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) { diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs index cdbd403e4..e7686ec64 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs @@ -11,7 +11,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Test { - [RequiresPreviewFeatures] public class StreamingResponseBodyFeatureTests { // Helper: creates a StreamingResponseBodyFeature backed by a MemoryStream stand-in. From 92d4f083bfb214ec7afe96525dd62940d5a3f157 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 8 Jul 2026 09:27:33 -0700 Subject: [PATCH 02/11] Address PR comments --- .autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json | 4 ++-- .../Internal/StreamingResponseBodyFeature.cs | 1 - Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs | 5 +++-- .../ResponseStreaming/HttpResponseStreamPrelude.cs | 1 - .../ResponseStreaming/LambdaResponseStream.cs | 1 - .../ResponseStreaming/LambdaResponseStreamFactory.cs | 1 - .../ResponseStreamingHostingTests.cs | 2 -- .../ResponseStreamingPropertyTests.cs | 2 -- .../BuildStreamingPreludeTests.cs | 1 - .../ResponseStreamingPropertyTests.cs | 1 - .../StreamingFunctionHandlerAsyncTests.cs | 2 -- .../StreamingResponseBodyFeatureTests.cs | 1 - 12 files changed, 5 insertions(+), 17 deletions(-) diff --git a/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json b/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json index 691797349..4b47d49fa 100644 --- a/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json +++ b/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json @@ -19,7 +19,7 @@ "Name": "Amazon.Lambda.AspNetCoreServer", "Type": "Minor", "ChangelogMessages": [ - "Removed RequiresPreviewFeatures attribute for Lambda Response Streaming." + "Removed RequiresPreviewFeatures attribute for Lambda Response Streaming.", "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support." ] }, @@ -27,7 +27,7 @@ "Name": "Amazon.Lambda.AspNetCoreServer.Hosting", "Type": "Minor", "ChangelogMessages": [ - "Removed RequiresPreviewFeatures attribute for Lambda Response Streaming." + "Removed RequiresPreviewFeatures attribute for Lambda Response Streaming.", "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support." ] }, diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs index 22084f4b0..86678b50e 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs @@ -3,7 +3,6 @@ using System; using System.IO; using System.IO.Pipelines; -using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; diff --git a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs index de5ddc407..bba419e0f 100644 --- a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs @@ -1,6 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using System; -using System.Reflection.Emit; -using System.Runtime.Versioning; using System.Text; namespace Amazon.Lambda.Core diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs index 4ac36491a..6f8bf2a4a 100644 --- a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Net; -using System.Runtime.Versioning; using System.Text.Json; namespace Amazon.Lambda.Core.ResponseStreaming diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs index f61bb7f3f..38b95c129 100644 --- a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs @@ -4,7 +4,6 @@ using System; using System.IO; -using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs index 29bddfa05..1c76e837e 100644 --- a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs @@ -3,7 +3,6 @@ #if NET8_0_OR_GREATER using System; using System.IO; -using System.Runtime.Versioning; namespace Amazon.Lambda.Core.ResponseStreaming { diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs index b9718aca9..8d0717928 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - -using System.Runtime.Versioning; using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; using Amazon.Lambda.AspNetCoreServer.Test; using Amazon.Lambda.Core; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs index 0717e29cd..853f0a0dc 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs @@ -1,8 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -using System.Runtime.Versioning; - using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs index 2e4122b1b..104f0d922 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using System.Net; -using System.Runtime.Versioning; using Amazon.Lambda.AspNetCoreServer.Internal; using Microsoft.AspNetCore.Http.Features; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs index 013c015a3..b1dbf95cb 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs index ef5c9711a..3bf4e2073 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs @@ -4,9 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; -using System.Runtime.Versioning; using System.Text; -using System.Threading; using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs index e7686ec64..669529f9a 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using System; using System.IO; -using System.Runtime.Versioning; using System.Threading.Tasks; using Amazon.Lambda.AspNetCoreServer.Internal; From df498793084c52aee041c5549da85ed00f601dd7 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 8 Jul 2026 11:03:12 -0700 Subject: [PATCH 03/11] Address PR comments --- .../BuildStreamingPreludeTests.cs | 311 ++++++++++++------ 1 file changed, 215 insertions(+), 96 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs index 104f0d922..7bca22fa4 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs @@ -1,5 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Collections.Generic; +using System.Linq; using System.Net; using Amazon.Lambda.AspNetCoreServer.Internal; @@ -10,12 +12,27 @@ namespace Amazon.Lambda.AspNetCoreServer.Test { public class BuildStreamingPreludeTests { + /// + /// Selects which proxy function implementation of BuildStreamingPrelude to exercise. + /// The two implementations differ in how non-cookie headers are represented on the prelude: + /// the HTTP API v2 function uses the single-value Headers collection while the + /// REST API function uses the multi-value MultiValueHeaders collection. + /// + public enum ProxyFunctionType + { + /// APIGatewayProxyFunction (REST API) - populates MultiValueHeaders. + RestApi, + + /// APIGatewayHttpApiV2ProxyFunction (HTTP API v2) - populates Headers. + HttpApiV2 + } + // Subclass that skips host startup entirely and // just exposes BuildStreamingPrelude directly without needing a running host. - private class StandalonePreludeBuilder : APIGatewayHttpApiV2ProxyFunction + private class HttpApiV2PreludeBuilder : APIGatewayHttpApiV2ProxyFunction { // Use the StartupMode.FirstRequest constructor so no host is started eagerly. - public StandalonePreludeBuilder() + public HttpApiV2PreludeBuilder() : base(StartupMode.FirstRequest) { } public Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude @@ -23,10 +40,36 @@ public Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude => BuildStreamingPrelude(responseFeature); } - private static StandalonePreludeBuilder CreateBuilder() => new StandalonePreludeBuilder(); + // Subclass that skips host startup entirely and + // just exposes BuildStreamingPrelude directly without needing a running host. + private class RestApiPreludeBuilder : APIGatewayProxyFunction + { + // Use the StartupMode.FirstRequest constructor so no host is started eagerly. + public RestApiPreludeBuilder() + : base(StartupMode.FirstRequest) { } + + public Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude + InvokeBuildStreamingPrelude(IHttpResponseFeature responseFeature) + => BuildStreamingPrelude(responseFeature); + } + + // Helper: invoke BuildStreamingPrelude on the requested proxy function implementation. + private static Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude BuildPrelude( + ProxyFunctionType functionType, IHttpResponseFeature responseFeature) + { + switch (functionType) + { + case ProxyFunctionType.RestApi: + return new RestApiPreludeBuilder().InvokeBuildStreamingPrelude(responseFeature); + case ProxyFunctionType.HttpApiV2: + return new HttpApiV2PreludeBuilder().InvokeBuildStreamingPrelude(responseFeature); + default: + throw new System.ArgumentOutOfRangeException(nameof(functionType)); + } + } // Helper: create an InvokeFeatures, set StatusCode and Headers, return as IHttpResponseFeature. - private static IHttpResponseFeature MakeResponseFeature(int statusCode, System.Collections.Generic.Dictionary headers = null) + private static IHttpResponseFeature MakeResponseFeature(int statusCode, Dictionary headers = null) { var features = new InvokeFeatures(); var rf = (IHttpResponseFeature)features; @@ -39,29 +82,100 @@ private static IHttpResponseFeature MakeResponseFeature(int statusCode, System.C return rf; } + // ----------------------------------------------------------------------- + // Assertion helpers that check the correct header collection depending on + // the proxy function type: MultiValueHeaders for REST API, Headers for HTTP API v2. + // ----------------------------------------------------------------------- + + // Assert a non-cookie header is present with the given values. + private static void AssertHeaderPresent( + Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude, + ProxyFunctionType functionType, string key, params string[] expectedValues) + { + if (functionType == ProxyFunctionType.RestApi) + { + Assert.True(prelude.MultiValueHeaders.ContainsKey(key)); + Assert.Equal(expectedValues, prelude.MultiValueHeaders[key]); + + // The single-value Headers collection is not used by the REST API implementation. + Assert.False(prelude.Headers.ContainsKey(key)); + } + else + { + Assert.True(prelude.Headers.ContainsKey(key)); + // HTTP API v2 uses single-value headers, so multiple values are joined with ", ". + Assert.Equal(string.Join(", ", expectedValues), prelude.Headers[key]); + + // The multi-value MultiValueHeaders collection is not used by the HTTP API v2 implementation. + Assert.False(prelude.MultiValueHeaders.ContainsKey(key)); + } + } + + // Assert a header is absent from whichever collection the implementation uses. + private static void AssertHeaderAbsent( + Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude, + ProxyFunctionType functionType, string key) + { + if (functionType == ProxyFunctionType.RestApi) + { + Assert.False(prelude.MultiValueHeaders.ContainsKey(key)); + } + else + { + Assert.False(prelude.Headers.ContainsKey(key)); + } + } + + // Assert the header collection used by the implementation is empty. + private static void AssertHeadersEmpty( + Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude, + ProxyFunctionType functionType) + { + if (functionType == ProxyFunctionType.RestApi) + { + Assert.Empty(prelude.MultiValueHeaders); + } + else + { + Assert.Empty(prelude.Headers); + } + } + // ----------------------------------------------------------------------- // 6.1 Status code is copied correctly for values 100–599 // ----------------------------------------------------------------------- [Theory] - [InlineData(100)] - [InlineData(200)] - [InlineData(201)] - [InlineData(204)] - [InlineData(301)] - [InlineData(302)] - [InlineData(400)] - [InlineData(401)] - [InlineData(403)] - [InlineData(404)] - [InlineData(500)] - [InlineData(503)] - [InlineData(599)] - public void StatusCode_IsCopiedCorrectly(int statusCode) + [InlineData(ProxyFunctionType.RestApi, 100)] + [InlineData(ProxyFunctionType.RestApi, 200)] + [InlineData(ProxyFunctionType.RestApi, 201)] + [InlineData(ProxyFunctionType.RestApi, 204)] + [InlineData(ProxyFunctionType.RestApi, 301)] + [InlineData(ProxyFunctionType.RestApi, 302)] + [InlineData(ProxyFunctionType.RestApi, 400)] + [InlineData(ProxyFunctionType.RestApi, 401)] + [InlineData(ProxyFunctionType.RestApi, 403)] + [InlineData(ProxyFunctionType.RestApi, 404)] + [InlineData(ProxyFunctionType.RestApi, 500)] + [InlineData(ProxyFunctionType.RestApi, 503)] + [InlineData(ProxyFunctionType.RestApi, 599)] + [InlineData(ProxyFunctionType.HttpApiV2, 100)] + [InlineData(ProxyFunctionType.HttpApiV2, 200)] + [InlineData(ProxyFunctionType.HttpApiV2, 201)] + [InlineData(ProxyFunctionType.HttpApiV2, 204)] + [InlineData(ProxyFunctionType.HttpApiV2, 301)] + [InlineData(ProxyFunctionType.HttpApiV2, 302)] + [InlineData(ProxyFunctionType.HttpApiV2, 400)] + [InlineData(ProxyFunctionType.HttpApiV2, 401)] + [InlineData(ProxyFunctionType.HttpApiV2, 403)] + [InlineData(ProxyFunctionType.HttpApiV2, 404)] + [InlineData(ProxyFunctionType.HttpApiV2, 500)] + [InlineData(ProxyFunctionType.HttpApiV2, 503)] + [InlineData(ProxyFunctionType.HttpApiV2, 599)] + public void StatusCode_IsCopiedCorrectly(ProxyFunctionType functionType, int statusCode) { - var builder = CreateBuilder(); var rf = MakeResponseFeature(statusCode); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); Assert.Equal((HttpStatusCode)statusCode, prelude.StatusCode); } @@ -69,113 +183,114 @@ public void StatusCode_IsCopiedCorrectly(int statusCode) // ----------------------------------------------------------------------- // 6.2 Status code defaults to 200 when IHttpResponseFeature.StatusCode is 0 // ----------------------------------------------------------------------- - [Fact] - public void StatusCode_DefaultsTo200_WhenFeatureStatusCodeIsZero() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void StatusCode_DefaultsTo200_WhenFeatureStatusCodeIsZero(ProxyFunctionType functionType) { - var builder = CreateBuilder(); var rf = MakeResponseFeature(0); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); Assert.Equal(HttpStatusCode.OK, prelude.StatusCode); } // ----------------------------------------------------------------------- - // 6.3 Non-Set-Cookie headers appear in Headers with all values preserved. - // HTTP API v2 uses the single-value "headers" collection (not "multiValueHeaders"), - // so multiple values for the same header are joined with ", ". + // 6.3 Non-Set-Cookie headers appear in the header collection with all values preserved. + // HTTP API v2 uses the single-value "headers" collection (joining multiple values with ", "), + // while the REST API uses the multi-value "multiValueHeaders" collection. // ----------------------------------------------------------------------- - [Fact] - public void NonSetCookieHeaders_AppearInHeaders_WithAllValuesPreserved() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void NonSetCookieHeaders_AppearInHeaders_WithAllValuesPreserved(ProxyFunctionType functionType) { - var builder = CreateBuilder(); - var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + var rf = MakeResponseFeature(200, new Dictionary { ["Content-Type"] = new[] { "application/json" }, ["X-Custom"] = new[] { "val1", "val2" }, ["Cache-Control"] = new[] { "no-cache", "no-store" } }); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); - Assert.True(prelude.Headers.ContainsKey("Content-Type")); - Assert.Equal("application/json", prelude.Headers["Content-Type"]); - - Assert.True(prelude.Headers.ContainsKey("X-Custom")); - Assert.Equal("val1, val2", prelude.Headers["X-Custom"]); - - Assert.True(prelude.Headers.ContainsKey("Cache-Control")); - Assert.Equal("no-cache, no-store", prelude.Headers["Cache-Control"]); + AssertHeaderPresent(prelude, functionType, "Content-Type", "application/json"); + AssertHeaderPresent(prelude, functionType, "X-Custom", "val1", "val2"); + AssertHeaderPresent(prelude, functionType, "Cache-Control", "no-cache", "no-store"); } - [Fact] - public void NonSetCookieHeaders_Headers_JoinsMultipleValues() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void NonSetCookieHeaders_JoinsMultipleValues(ProxyFunctionType functionType) { - var builder = CreateBuilder(); - var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + var rf = MakeResponseFeature(200, new Dictionary { ["Accept"] = new[] { "text/html", "application/xhtml+xml", "application/xml" } }); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); - Assert.Equal("text/html, application/xhtml+xml, application/xml", - prelude.Headers["Accept"]); + AssertHeaderPresent(prelude, functionType, "Accept", + "text/html", "application/xhtml+xml", "application/xml"); } // ----------------------------------------------------------------------- - // 6.4 Set-Cookie header values are moved to Cookies and absent from MultiValueHeaders + // 6.4 Set-Cookie header values are moved to Cookies and absent from the header collection // ----------------------------------------------------------------------- - [Fact] - public void SetCookieHeader_MovedToCookies_AbsentFromMultiValueHeaders() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void SetCookieHeader_MovedToCookies_AbsentFromHeaders(ProxyFunctionType functionType) { - var builder = CreateBuilder(); - var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + var rf = MakeResponseFeature(200, new Dictionary { ["Set-Cookie"] = new[] { "session=abc123; Path=/; HttpOnly" }, ["Content-Type"] = new[] { "text/html" } }); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); // Cookie value is in Cookies Assert.Contains("session=abc123; Path=/; HttpOnly", prelude.Cookies); - // Set-Cookie is NOT in Headers - Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); - Assert.False(prelude.Headers.ContainsKey("set-cookie")); + // Set-Cookie is NOT in the header collection + AssertHeaderAbsent(prelude, functionType, "Set-Cookie"); + AssertHeaderAbsent(prelude, functionType, "set-cookie"); // Other headers are still present - Assert.True(prelude.Headers.ContainsKey("Content-Type")); + AssertHeaderPresent(prelude, functionType, "Content-Type", "text/html"); } - [Fact] - public void SetCookieHeader_IsCaseInsensitive() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void SetCookieHeader_IsCaseInsensitive(ProxyFunctionType functionType) { // The implementation uses StringComparison.OrdinalIgnoreCase, so // "set-cookie" (lowercase) should also be routed to Cookies. - var builder = CreateBuilder(); var features = new InvokeFeatures(); var rf = (IHttpResponseFeature)features; rf.StatusCode = 200; // HeaderDictionary is case-insensitive, so "set-cookie" and "Set-Cookie" are the same key. rf.Headers["set-cookie"] = new Microsoft.Extensions.Primitives.StringValues("id=xyz; Path=/"); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); Assert.Contains("id=xyz; Path=/", prelude.Cookies); - Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); - Assert.False(prelude.Headers.ContainsKey("set-cookie")); + AssertHeaderAbsent(prelude, functionType, "Set-Cookie"); + AssertHeaderAbsent(prelude, functionType, "set-cookie"); } // ----------------------------------------------------------------------- // 6.5 Multiple Set-Cookie values all appear in Cookies // ----------------------------------------------------------------------- - [Fact] - public void MultipleSetCookieValues_AllAppearInCookies() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void MultipleSetCookieValues_AllAppearInCookies(ProxyFunctionType functionType) { - var builder = CreateBuilder(); - var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + var rf = MakeResponseFeature(200, new Dictionary { ["Set-Cookie"] = new[] { @@ -185,83 +300,87 @@ public void MultipleSetCookieValues_AllAppearInCookies() } }); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); Assert.Equal(3, prelude.Cookies.Count); Assert.Contains("session=abc; Path=/; HttpOnly", prelude.Cookies); Assert.Contains("theme=dark; Path=/", prelude.Cookies); Assert.Contains("lang=en; Path=/; SameSite=Strict", prelude.Cookies); - // None of them should be in Headers - Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); + // None of them should be in the header collection + AssertHeaderAbsent(prelude, functionType, "Set-Cookie"); } - [Fact] - public void MultipleSetCookieValues_WithOtherHeaders_CookiesAndHeadersAreSeparated() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void MultipleSetCookieValues_WithOtherHeaders_CookiesAndHeadersAreSeparated(ProxyFunctionType functionType) { - var builder = CreateBuilder(); - var rf = MakeResponseFeature(201, new System.Collections.Generic.Dictionary + var rf = MakeResponseFeature(201, new Dictionary { ["Set-Cookie"] = new[] { "a=1", "b=2" }, ["Location"] = new[] { "/new-resource" } }); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); Assert.Equal((HttpStatusCode)201, prelude.StatusCode); Assert.Equal(2, prelude.Cookies.Count); Assert.Contains("a=1", prelude.Cookies); Assert.Contains("b=2", prelude.Cookies); - Assert.True(prelude.Headers.ContainsKey("Location")); - Assert.False(prelude.Headers.ContainsKey("Set-Cookie")); + AssertHeaderPresent(prelude, functionType, "Location", "/new-resource"); + AssertHeaderAbsent(prelude, functionType, "Set-Cookie"); } - [Fact] - public void EmptyHeaders_ProducesEmptyMultiValueHeadersAndCookies() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void EmptyHeaders_ProducesEmptyHeadersAndCookies(ProxyFunctionType functionType) { - var builder = CreateBuilder(); var rf = MakeResponseFeature(204); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); Assert.Equal(HttpStatusCode.NoContent, prelude.StatusCode); - Assert.Empty(prelude.Headers); + AssertHeadersEmpty(prelude, functionType); Assert.Empty(prelude.Cookies); } // ----------------------------------------------------------------------- // Content-Length and Transfer-Encoding are excluded from the prelude // ----------------------------------------------------------------------- - [Fact] - public void ContentLengthHeader_ExcludedFromPrelude() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void ContentLengthHeader_ExcludedFromPrelude(ProxyFunctionType functionType) { - var builder = CreateBuilder(); - var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + var rf = MakeResponseFeature(200, new Dictionary { ["Content-Type"] = new[] { "application/json" }, ["Content-Length"] = new[] { "42" } }); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); - Assert.True(prelude.Headers.ContainsKey("Content-Type")); - Assert.False(prelude.Headers.ContainsKey("Content-Length")); + AssertHeaderPresent(prelude, functionType, "Content-Type", "application/json"); + AssertHeaderAbsent(prelude, functionType, "Content-Length"); } - [Fact] - public void TransferEncodingHeader_ExcludedFromPrelude() + [Theory] + [InlineData(ProxyFunctionType.RestApi)] + [InlineData(ProxyFunctionType.HttpApiV2)] + public void TransferEncodingHeader_ExcludedFromPrelude(ProxyFunctionType functionType) { - var builder = CreateBuilder(); - var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + var rf = MakeResponseFeature(200, new Dictionary { ["Content-Type"] = new[] { "text/plain" }, ["Transfer-Encoding"] = new[] { "chunked" } }); - var prelude = builder.InvokeBuildStreamingPrelude(rf); + var prelude = BuildPrelude(functionType, rf); - Assert.True(prelude.Headers.ContainsKey("Content-Type")); - Assert.False(prelude.Headers.ContainsKey("Transfer-Encoding")); + AssertHeaderPresent(prelude, functionType, "Content-Type", "text/plain"); + AssertHeaderAbsent(prelude, functionType, "Transfer-Encoding"); } } } From 093441b7502e526e6f35a7c8f456ddbaab994c91 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 20 Jul 2026 11:28:35 -0700 Subject: [PATCH 04/11] Add the ASP.NET Core unit tests to CI targets --- buildtools/build.proj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/buildtools/build.proj b/buildtools/build.proj index e160116aa..32f156103 100644 --- a/buildtools/build.proj +++ b/buildtools/build.proj @@ -218,6 +218,8 @@ + + + $(MSBuildProjectDirectory)\test.runsettings diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs index 683850431..13e5935f1 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs @@ -135,26 +135,6 @@ public void AddAWSLambdaHosting_ApplicationLoadBalancer_RegistersHostingOptions( Assert.True(hostingOptions.ContentTypeEncodings.ContainsKey("image/png")); } - [Fact] - public void AddAWSLambdaHosting_NotInLambda_DoesNotRegisterHostingOptions() - { - // Arrange - var services = new ServiceCollection(); - // No AWS_LAMBDA_FUNCTION_NAME environment variable set - - // Act - services.AddAWSLambdaHosting(LambdaEventSource.HttpApi, options => - { - options.DefaultResponseContentEncoding = ResponseContentEncoding.Base64; - }); - - var serviceProvider = services.BuildServiceProvider(); - - // Assert - var hostingOptions = serviceProvider.GetService(); - Assert.Null(hostingOptions); - } - [Fact] public void AddAWSLambdaHosting_ConfigurationIsApplied() { diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/test.runsettings b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/test.runsettings new file mode 100644 index 000000000..3e5f6bdf8 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/test.runsettings @@ -0,0 +1,20 @@ + + + + + + 1 + + + diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj index 30a0c672e..035cbe4b2 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj @@ -12,6 +12,8 @@ false false 1701;1702;1705;CS0618;CS1591 + + $(MSBuildProjectDirectory)\test.runsettings diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/test.runsettings b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/test.runsettings new file mode 100644 index 000000000..e2ac6071a --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/test.runsettings @@ -0,0 +1,21 @@ + + + + + + 1 + + + From 5a130d3c21a23cc119e3eee1f07f228aa5b7035c Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Tue, 21 Jul 2026 10:50:14 -0700 Subject: [PATCH 07/11] More test stabilization --- .../TestMinimalAPI.cs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs index 22709a1e3..80efda32c 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs @@ -55,15 +55,40 @@ public T ExecuteRequest(string eventFilePath) processStartInfo.Arguments = $"{comamndArgument} dotnet run \"{requestFilePath}\" \"{responseFilePath}\""; processStartInfo.WorkingDirectory = GetTestAppDirectory(); + // Capture stdout/stderr from the "dotnet run" shell out so that, when it exits non-zero, the + // underlying build/runtime output is surfaced in the test failure instead of just an exit code. + processStartInfo.UseShellExecute = false; + processStartInfo.RedirectStandardOutput = true; + processStartInfo.RedirectStandardError = true; + lock (lock_process) { using var process = Process.Start(processStartInfo); - process.WaitForExit(15000); + + // Read both streams asynchronously; reading them synchronously can deadlock if one pipe's + // buffer fills while we're blocked waiting on the other. + var stdout = new StringBuilder(); + var stderr = new StringBuilder(); + process.OutputDataReceived += (sender, e) => { if (e.Data != null) stdout.AppendLine(e.Data); }; + process.ErrorDataReceived += (sender, e) => { if (e.Data != null) stderr.AppendLine(e.Data); }; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + if (!process.WaitForExit(45000)) + { + try { process.Kill(); } catch { /* best effort */ } + throw new Exception( + "Process timed out after 45000ms." + BuildProcessOutput(stdout, stderr)); + } + + // Ensure the asynchronous output handlers have flushed all buffered data before we read it. + process.WaitForExit(); if (process.ExitCode != 0) { - throw new Exception("Process failed with exit code: " + process.ExitCode); + throw new Exception( + "Process failed with exit code: " + process.ExitCode + BuildProcessOutput(stdout, stderr)); } if(!File.Exists(responseFilePath)) @@ -80,6 +105,12 @@ public T ExecuteRequest(string eventFilePath) } } + private static string BuildProcessOutput(StringBuilder stdout, StringBuilder stderr) + { + return $"{Environment.NewLine}--- STDOUT ---{Environment.NewLine}{stdout}" + + $"{Environment.NewLine}--- STDERR ---{Environment.NewLine}{stderr}"; + } + private string GetTestAppDirectory() { var path = GetType().GetTypeInfo().Assembly.Location; From 66be05cadc430cc2c60ba31ea29b0122c827e768 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Tue, 21 Jul 2026 15:40:33 -0700 Subject: [PATCH 08/11] More test stabilization --- .../TestMinimalAPI.cs | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs index 80efda32c..4ef4ad819 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs @@ -49,13 +49,19 @@ public T ExecuteRequest(string eventFilePath) var requestFilePath = Path.Combine(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location), eventFilePath); var responseFilePath = Path.GetTempFileName(); - var comamndArgument = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"/c" : $"-c"; + // Invoke "dotnet" directly rather than through a shell. Routing "dotnet run ..." through a + // shell as a single Arguments string is not portable: on Unix .NET splits the string into an + // argv array, so "sh -c dotnet run " makes the shell run just "dotnet" (with the + // rest as positional parameters $0/$1/...), which prints the dotnet usage text and exits 129. + // Passing each argument via ArgumentList lets the runtime quote them correctly on every OS. ProcessStartInfo processStartInfo = new ProcessStartInfo(); - processStartInfo.FileName = GetSystemShell(); - processStartInfo.Arguments = $"{comamndArgument} dotnet run \"{requestFilePath}\" \"{responseFilePath}\""; + processStartInfo.FileName = "dotnet"; + processStartInfo.ArgumentList.Add("run"); + processStartInfo.ArgumentList.Add(requestFilePath); + processStartInfo.ArgumentList.Add(responseFilePath); processStartInfo.WorkingDirectory = GetTestAppDirectory(); - // Capture stdout/stderr from the "dotnet run" shell out so that, when it exits non-zero, the + // Capture stdout/stderr from the "dotnet run" child process so that, when it exits non-zero, the // underlying build/runtime output is surfaced in the test failure instead of just an exit code. processStartInfo.UseShellExecute = false; processStartInfo.RedirectStandardOutput = true; @@ -121,28 +127,6 @@ private string GetTestAppDirectory() return Path.GetFullPath(Path.Combine(path, "TestMinimalAPIApp")); } - - private string GetSystemShell() - { - if (TryGetEnvironmentVariable("COMSPEC", out var comspec)) - { - return comspec!; - } - - if (TryGetEnvironmentVariable("SHELL", out var shell)) - { - return shell!; - } - - // fallback to defaults - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/bin/sh"; - } - - private bool TryGetEnvironmentVariable(string variable, out string value) - { - value = Environment.GetEnvironmentVariable(variable); - return !string.IsNullOrEmpty(value); - } } } } From d47cba11a2c6018177901173fe31bf921af275a3 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 23 Jul 2026 16:17:15 -0700 Subject: [PATCH 09/11] More test stabilization --- .../9e75413c-9cb6-4521-aaca-09b557fff2b3.json | 3 +- .../AbstractAspNetCoreFunction.cs | 43 +------------------ .../AddAWSLambdaBeforeSnapshotRequestTests.cs | 1 + ...mbda.AspNetCoreServer.Hosting.Tests.csproj | 2 - .../ResponseStreamingHostingTests.cs | 1 + .../ServiceCollectionExtensionsTests.cs | 1 + .../test.runsettings | 20 --------- ...Amazon.Lambda.AspNetCoreServer.Test.csproj | 2 - .../ResponseStreamingPropertyTests.cs | 12 ++++-- .../StreamingFunctionHandlerAsyncTests.cs | 36 +++++++++------- .../TestApiGatewayHttpApiV2Calls.cs | 4 +- .../TestApiGatewayWebsocketApiCalls.cs | 2 +- .../TestApplicationLoadBalancerCalls.cs | 2 +- .../TestCallingWebAPI.cs | 2 +- .../test.runsettings | 21 --------- .../test/TestWebApp/ALBLambdaFunction.cs | 5 ++- .../TestWebApp/ApiGatewayLambdaFunction.cs | 5 ++- .../TestWebApp/HttpApiV2LambdaFunction.cs | 5 +++ .../TestWebApp/WebsocketApiLambdaFunction.cs | 8 +++- 19 files changed, 59 insertions(+), 116 deletions(-) delete mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/test.runsettings delete mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/test.runsettings diff --git a/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json b/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json index 2857823b1..4b47d49fa 100644 --- a/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json +++ b/.autover/changes/9e75413c-9cb6-4521-aaca-09b557fff2b3.json @@ -20,8 +20,7 @@ "Type": "Minor", "ChangelogMessages": [ "Removed RequiresPreviewFeatures attribute for Lambda Response Streaming.", - "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support.", - "Updated the base class AbstractAspNetCoreFunction to implement IDisposable. This is mostly needed for unit tests. In the Lambda environment there will only be one instance of AbstractAspNetCoreFunction subclass created." + "Updated Amazon.Lambda.Core dependency to include Lambda Response Streaming support." ] }, { diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index acdc75f61..55b69f67f 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs @@ -41,21 +41,13 @@ public abstract class AbstractAspNetCoreFunction /// /// /// - public abstract class AbstractAspNetCoreFunction : AbstractAspNetCoreFunction, IDisposable + public abstract class AbstractAspNetCoreFunction : AbstractAspNetCoreFunction { private protected IServiceProvider _hostServices; private protected LambdaServer _server; private protected ILogger _logger; private protected StartupMode _startupMode; - // The IHost created and owned by this instance in Start(). Retained so it can be disposed, - // releasing resources such as the configuration file-change watchers (inotify instances on Linux) - // that Host.CreateDefaultBuilder registers. Null when the function is constructed from an - // externally-owned IServiceProvider, in which case this instance does not own the host. - private IHost _host; - - private bool _disposed; - // Defines a mapping from registered content types to the response encoding format // which dictates what transformations should be applied before returning response content private readonly Dictionary _responseContentEncodingForContentType = new Dictionary @@ -365,7 +357,6 @@ protected void Start() }); var host = builder.Build(); - _host = host; PostCreateHost(host); host.Start(); @@ -823,37 +814,5 @@ private async Task RunPipelineAsync(object context, InvokeFeatures features) throw; } } - - /// - /// Disposes the IHost created and owned by this instance in , releasing - /// resources it holds such as the configuration file-change watchers (inotify instances on Linux) - /// registered by Host.CreateDefaultBuilder. When the function was constructed from an - /// externally-owned IServiceProvider this instance does not own a host and disposal is a no-op. - /// - /// true when called from . - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _host?.Dispose(); - _host = null; - } - - _disposed = true; - } - - /// - /// Disposes the resources owned by this function, including the IHost created in . - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } } diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs index 3505d8bb3..a25aea033 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs @@ -12,6 +12,7 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; /// /// Tests for /// +[Collection(EnvironmentVariableCollection.Name)] public class AddAWSLambdaBeforeSnapshotRequestTests { [Theory] diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj index 346dff4f6..705fdabd5 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj @@ -11,8 +11,6 @@ false false 1701;1702;1705;CS0618;CS1591 - - $(MSBuildProjectDirectory)\test.runsettings diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs index 8d0717928..ce9fc3fb5 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs @@ -14,6 +14,7 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; /// /// Tests for response streaming integration in hosting (Requirement 10). /// +[Collection(EnvironmentVariableCollection.Name)] public class ResponseStreamingHostingTests { [Fact] diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs index 13e5935f1..23eb4def0 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ServiceCollectionExtensionsTests.cs @@ -11,6 +11,7 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; /// /// Tests for service registration in /// +[Collection(EnvironmentVariableCollection.Name)] public class ServiceCollectionExtensionsTests { [Fact] diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/test.runsettings b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/test.runsettings deleted file mode 100644 index 3e5f6bdf8..000000000 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/test.runsettings +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - 1 - - - diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj index 035cbe4b2..30a0c672e 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj @@ -12,8 +12,6 @@ false false 1701;1702;1705;CS0618;CS1591 - - $(MSBuildProjectDirectory)\test.runsettings diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs index d3e93f6ea..0cadaab15 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs @@ -37,6 +37,9 @@ public PropertyTestStreamingFunction() EnableResponseStreaming = true; } + protected override void Init(Microsoft.Extensions.Hosting.IHostBuilder builder) + => TestWebApp.DisableConfigFileWatching.Apply(builder); + public void PublicMarshallRequest(InvokeFeatures features, APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) => MarshallRequest(features, request, context); @@ -107,7 +110,7 @@ public static IEnumerable RequestMarshallingCases() => public async Task Property1_RequestMarshalling_IdenticalInStreamingAndBufferedModes( string method, string path, Dictionary headers, string body) { - using var function = new PropertyTestStreamingFunction(); + var function = new PropertyTestStreamingFunction(); var context = new TestLambdaContext(); // Warm up so the host is started @@ -152,7 +155,7 @@ public async Task Property2_BufferedMode_Unaffected( string method, string path, Dictionary headers, string body) { // Use a fresh function with streaming OFF - using var function = new PropertyTestStreamingFunction(); + var function = new PropertyTestStreamingFunction(); function.EnableResponseStreaming = false; var context = new TestLambdaContext(); @@ -358,7 +361,7 @@ public async Task Property8_OnCompletedCallbacks_FireAfterStreamClose(int cbCoun var completedSequences = new List(); int streamClosedSequence = -1; - using var function = new OnCompletedTrackingFunction( + var function = new OnCompletedTrackingFunction( cbCount: cbCount, completedSequences: completedSequences, getAndIncrementCounter: () => sequenceCounter++, @@ -427,6 +430,9 @@ public OnCompletedTrackingFunction( _onStreamClosed = onStreamClosed; } + protected override void Init(Microsoft.Extensions.Hosting.IHostBuilder builder) + => TestWebApp.DisableConfigFileWatching.Apply(builder); + protected override void PostMarshallItemsFeatureFeature( IItemsFeature aspNetCoreItemFeature, APIGatewayHttpApiV2ProxyRequest lambdaRequest, diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs index 154e067b8..5120661ec 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs @@ -57,6 +57,9 @@ public TestableStreamingFunction() EnableResponseStreaming = true; } + protected override void Init(Microsoft.Extensions.Hosting.IHostBuilder builder) + => TestWebApp.DisableConfigFileWatching.Apply(builder); + // Expose MarshallRequest publicly so tests can call it after the host is started public void PublicMarshallRequest(InvokeFeatures features, APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) @@ -123,7 +126,7 @@ private static APIGatewayHttpApiV2ProxyRequest MakeRequest( [Fact] public async Task RequestMarshalling_ProducesSameHttpRequestFeatureState_AsBufferedMode() { - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); var context = new TestLambdaContext(); var request = MakeRequest( method: "POST", @@ -156,7 +159,7 @@ public async Task RequestMarshalling_ProducesSameHttpRequestFeatureState_AsBuffe [Fact] public async Task RequestMarshalling_PreservesHeaders_InStreamingMode() { - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); var context = new TestLambdaContext(); var request = MakeRequest( headers: new Dictionary @@ -188,7 +191,7 @@ public async Task AfterSetup_BodyFeature_IsStreamingResponseBodyFeature() { IHttpResponseBodyFeature capturedBodyFeature = null; - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); function.PipelineSetupAction = features => { var responseFeature = (IHttpResponseFeature)features; @@ -215,7 +218,7 @@ public async Task AfterSetup_BodyFeature_IsStreamingResponseBodyFeature_ViaOnSta { IHttpResponseBodyFeature capturedBodyFeature = null; - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); function.PipelineSetupAction = features => { var responseFeature = (IHttpResponseFeature)features; @@ -247,7 +250,7 @@ public async Task AfterSetup_BodyFeature_IsStreamingResponseBodyFeature_ViaOnSta public async Task FunctionHandlerAsync_BufferedMode_StillReturnsResponse_ViaMarshallResponse() { // Buffered mode: EnableResponseStreaming defaults to false - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); function.EnableResponseStreaming = false; var context = new TestLambdaContext(); var request = MakeRequest(); @@ -263,7 +266,7 @@ public async Task FunctionHandlerAsync_BufferedMode_StillReturnsResponse_ViaMars [Fact] public async Task FunctionHandlerAsync_BufferedMode_ReturnsStatusCode_FromPipeline() { - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); function.EnableResponseStreaming = false; var context = new TestLambdaContext(); var request = MakeRequest(path: "/api/values"); @@ -276,7 +279,7 @@ public async Task FunctionHandlerAsync_BufferedMode_ReturnsStatusCode_FromPipeli [Fact] public async Task FunctionHandlerAsync_BufferedMode_DoesNotOpenLambdaStream() { - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); function.EnableResponseStreaming = false; var context = new TestLambdaContext(); var request = MakeRequest(); @@ -296,7 +299,7 @@ public async Task OnCompleted_FiresAfterStreamClosed_OnSuccessPath() { bool callbackFired = false; - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); function.PipelineSetupAction = features => { var responseFeature = (IHttpResponseFeature)features; @@ -321,7 +324,7 @@ public async Task OnCompleted_MultipleCallbacks_AllFire() { int firedCount = 0; - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); function.PipelineSetupAction = features => { var responseFeature = (IHttpResponseFeature)features; @@ -349,7 +352,7 @@ public async Task ExceptionBeforeStreamOpen_StreamClosedCleanly_OnCompletedFires { bool onCompletedFired = false; - using var function = new ThrowingBeforeStreamOpenFunction( + var function = new ThrowingBeforeStreamOpenFunction( onCompleted: () => onCompletedFired = true); var context = new TestLambdaContext(); @@ -368,7 +371,7 @@ public async Task ExceptionBeforeStreamOpen_WithIncludeExceptionDetail_Writes500 { const string exceptionMessage = "Deliberate test failure for 500 response"; - using var function = new ThrowingBeforeStreamOpenFunction( + var function = new ThrowingBeforeStreamOpenFunction( exceptionMessage: exceptionMessage, onCompleted: null) { @@ -391,7 +394,7 @@ public async Task ExceptionBeforeStreamOpen_WithIncludeExceptionDetail_Writes500 [Fact] public async Task ExceptionBeforeStreamOpen_WithoutIncludeExceptionDetail_NoStreamOpened() { - using var function = new ThrowingBeforeStreamOpenFunction( + var function = new ThrowingBeforeStreamOpenFunction( exceptionMessage: "Should not appear in response", onCompleted: null) { @@ -415,7 +418,7 @@ public async Task ExceptionAfterStreamOpen_StreamClosedAfterLogging_OnCompletedF { bool onCompletedFired = false; - using var function = new ThrowingAfterStreamOpenFunction( + var function = new ThrowingAfterStreamOpenFunction( onCompleted: () => onCompletedFired = true); var context = new TestLambdaContext(); @@ -432,7 +435,7 @@ public async Task ExceptionAfterStreamOpen_StreamClosedAfterLogging_OnCompletedF [Fact] public async Task ExceptionAfterStreamOpen_DoesNotWriteNewErrorBody() { - using var function = new ThrowingAfterStreamOpenFunction(onCompleted: null) + var function = new ThrowingAfterStreamOpenFunction(onCompleted: null) { IncludeUnhandledExceptionDetailInResponse = true }; @@ -466,7 +469,7 @@ public void FunctionHandlerAsync_HasLambdaSerializerAttribute() [Fact] public void EnableResponseStreaming_Property_DefaultsToFalse() { - using var function = new TestableStreamingFunction(); + var function = new TestableStreamingFunction(); function.EnableResponseStreaming = false; // reset to default Assert.False(function.EnableResponseStreaming); } @@ -515,6 +518,9 @@ protected CustomPipelineStreamingFunction() EnableResponseStreaming = true; } + protected override void Init(Microsoft.Extensions.Hosting.IHostBuilder builder) + => TestWebApp.DisableConfigFileWatching.Apply(builder); + protected override Stream CreateLambdaResponseStream( Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) { diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs index 34d4ceef5..beaa40b52 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs @@ -294,7 +294,7 @@ public async Task TestSnapStartInitialization() var cts = new CancellationTokenSource(); - using var lambdaFunction = new TestWebApp.HttpV2LambdaFunction(); + var lambdaFunction = new TestWebApp.HttpV2LambdaFunction(); using var bootstrap = LambdaBootstrapBuilder.Create( lambdaFunction.FunctionHandlerAsync, new DefaultLambdaJsonSerializer()) @@ -323,7 +323,7 @@ private async Task InvokeAPIGatewayRequest(Tes private async Task InvokeAPIGatewayRequestWithContent(TestLambdaContext context, string requestContent, bool configureApiToReturnExceptionDetail = false) { - using var lambdaFunction = new TestWebApp.HttpV2LambdaFunction(); + var lambdaFunction = new TestWebApp.HttpV2LambdaFunction(); if (configureApiToReturnExceptionDetail) lambdaFunction.IncludeUnhandledExceptionDetailInResponse = true; var requestStream = new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(requestContent)); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs index 371bc4fe1..07d0551da 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs @@ -39,7 +39,7 @@ private async Task InvokeAPIGatewayRequest(TestLambdaCo private async Task InvokeAPIGatewayRequestWithContent(TestLambdaContext context, string requestContent, bool configureApiToReturnExceptionDetail = false) { - using var lambdaFunction = new TestWebApp.WebsocketLambdaFunction(); + var lambdaFunction = new TestWebApp.WebsocketLambdaFunction(); if (configureApiToReturnExceptionDetail) lambdaFunction.IncludeUnhandledExceptionDetailInResponse = true; var requestStream = new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(requestContent)); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApplicationLoadBalancerCalls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApplicationLoadBalancerCalls.cs index 052da164b..5a76d8b54 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApplicationLoadBalancerCalls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApplicationLoadBalancerCalls.cs @@ -242,7 +242,7 @@ private async Task InvokeApplicationLoadBalance private async Task InvokeApplicationLoadBalancerRequest(TestLambdaContext context, string fileName) { - using var lambdaFunction = new ALBLambdaFunction(); + var lambdaFunction = new ALBLambdaFunction(); var filePath = Path.Combine(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location), fileName); var requestStr = File.ReadAllText(filePath); var request = JsonConvert.DeserializeObject(requestStr); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs index f50ff843e..9743da7db 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs @@ -529,7 +529,7 @@ private async Task InvokeAPIGatewayRequest(TestLambdaCo private async Task InvokeAPIGatewayRequestWithContent(TestLambdaContext context, string requestContent, bool configureApiToReturnExceptionDetail = false) { - using var lambdaFunction = new ApiGatewayLambdaFunction(); + var lambdaFunction = new ApiGatewayLambdaFunction(); if (configureApiToReturnExceptionDetail) lambdaFunction.IncludeUnhandledExceptionDetailInResponse = true; var requestStream = new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(requestContent)); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/test.runsettings b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/test.runsettings deleted file mode 100644 index e2ac6071a..000000000 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/test.runsettings +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - 1 - - - diff --git a/Libraries/test/TestWebApp/ALBLambdaFunction.cs b/Libraries/test/TestWebApp/ALBLambdaFunction.cs index e3ed9c82d..f068f3a83 100644 --- a/Libraries/test/TestWebApp/ALBLambdaFunction.cs +++ b/Libraries/test/TestWebApp/ALBLambdaFunction.cs @@ -1,8 +1,11 @@ -using Amazon.Lambda.AspNetCoreServer; +using Amazon.Lambda.AspNetCoreServer; + +using Microsoft.Extensions.Hosting; namespace TestWebApp { public class ALBLambdaFunction : ApplicationLoadBalancerFunction { + protected override void Init(IHostBuilder builder) => DisableConfigFileWatching.Apply(builder); } } diff --git a/Libraries/test/TestWebApp/ApiGatewayLambdaFunction.cs b/Libraries/test/TestWebApp/ApiGatewayLambdaFunction.cs index 715df4188..0ef0f8f2d 100644 --- a/Libraries/test/TestWebApp/ApiGatewayLambdaFunction.cs +++ b/Libraries/test/TestWebApp/ApiGatewayLambdaFunction.cs @@ -1,8 +1,11 @@ -using Amazon.Lambda.AspNetCoreServer; +using Amazon.Lambda.AspNetCoreServer; + +using Microsoft.Extensions.Hosting; namespace TestWebApp { public class ApiGatewayLambdaFunction : APIGatewayProxyFunction { + protected override void Init(IHostBuilder builder) => DisableConfigFileWatching.Apply(builder); } } diff --git a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs index b8d88ccb1..7bb4bf3cc 100644 --- a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs +++ b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs @@ -2,10 +2,15 @@ using System.Net.Http; using System; using Amazon.Lambda.AspNetCoreServer; + +using Microsoft.Extensions.Hosting; + namespace TestWebApp { public class HttpV2LambdaFunction : APIGatewayHttpApiV2ProxyFunction { + protected override void Init(IHostBuilder builder) => DisableConfigFileWatching.Apply(builder); + protected override IEnumerable GetBeforeSnapshotRequests() => [ new HttpRequestMessage(HttpMethod.Get, "api/SnapStart") diff --git a/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs b/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs index 105302c7f..7d3308b19 100644 --- a/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs +++ b/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs @@ -1,7 +1,11 @@ -using Amazon.Lambda.AspNetCoreServer; +using Amazon.Lambda.AspNetCoreServer; + +using Microsoft.Extensions.Hosting; + namespace TestWebApp { public class WebsocketLambdaFunction : APIGatewayWebsocketApiProxyFunction { + protected override void Init(IHostBuilder builder) => DisableConfigFileWatching.Apply(builder); } -} \ No newline at end of file +} From 9fadd9581b7e6c2342067bd75ed7f243abd68d3a Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 23 Jul 2026 16:32:23 -0700 Subject: [PATCH 10/11] More test stabilization --- .../EnvironmentVariableCollection.cs | 20 +++++++++++ .../TestWebApp/DisableConfigFileWatching.cs | 35 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/EnvironmentVariableCollection.cs create mode 100644 Libraries/test/TestWebApp/DisableConfigFileWatching.cs diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/EnvironmentVariableCollection.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/EnvironmentVariableCollection.cs new file mode 100644 index 000000000..2be2eca15 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/EnvironmentVariableCollection.cs @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +/// +/// Groups tests that mutate the process-wide AWS_LAMBDA_FUNCTION_NAME (and related) environment +/// variables via . xUnit runs +/// all classes in a single collection sequentially, so applying [Collection(Name)] to these classes +/// prevents them from running in parallel and clobbering each other's environment variables - which +/// otherwise causes intermittent failures (for example a test that expects the variable to be unset +/// running while another test has it set). +/// +[CollectionDefinition(Name)] +public class EnvironmentVariableCollection +{ + public const string Name = "EnvironmentVariable"; +} diff --git a/Libraries/test/TestWebApp/DisableConfigFileWatching.cs b/Libraries/test/TestWebApp/DisableConfigFileWatching.cs new file mode 100644 index 000000000..218460796 --- /dev/null +++ b/Libraries/test/TestWebApp/DisableConfigFileWatching.cs @@ -0,0 +1,35 @@ +using System.Linq; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace TestWebApp +{ + /// + /// Test-only helper for turning off change tracking on file-based configuration sources (for example + /// the appsettings.json and appsettings.{Environment}.json files added by Host.CreateDefaultBuilder). + /// + /// The tests never care about reloading configuration in response to a file change. On Linux each + /// watched file consumes an inotify instance, and the per-user inotify limit is easily exhausted when + /// the tests create many ASP.NET Core hosts, producing "The configured user limit on the number of + /// inotify instances has been reached". Disabling the watchers avoids consuming inotify instances. + /// + /// + /// This lives in test support code only; the shipping Amazon.Lambda.AspNetCoreServer package is + /// unchanged. Lambda function classes used by the tests call this from their Init(IHostBuilder) override. + /// + /// + public static class DisableConfigFileWatching + { + public static void Apply(IHostBuilder builder) + { + builder.ConfigureAppConfiguration((context, config) => + { + foreach (var source in config.Sources.OfType()) + { + source.ReloadOnChange = false; + } + }); + } + } +} From 0b072213092576cc4e6c5f02ebe1ce75550dafea Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 23 Jul 2026 17:14:44 -0700 Subject: [PATCH 11/11] More test stabilization --- Libraries/test/TestWebApp/Startup.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Libraries/test/TestWebApp/Startup.cs b/Libraries/test/TestWebApp/Startup.cs index 1d0422c30..c7b4cf104 100644 --- a/Libraries/test/TestWebApp/Startup.cs +++ b/Libraries/test/TestWebApp/Startup.cs @@ -19,9 +19,13 @@ public class Startup #pragma warning disable CS0618 public Startup(IHostingEnvironment env) { + // reloadOnChange is intentionally false: the tests never need to react to appsettings.json + // changes, and on Linux each watched file consumes an inotify instance. The tests create many + // hosts, so watching would exhaust the per-user inotify limit ("The configured user limit (128) + // on the number of inotify instances has been reached"). var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); builder.AddEnvironmentVariables();