From 22d1469854413089fe883cf1dadc2b66f7adb860 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 11 Feb 2026 16:16:55 -0800 Subject: [PATCH 01/13] Task 1 --- .../Client/IResponseStream.cs | 85 +++++++++++++++++++ .../Client/ResponseStreamContext.cs | 44 ++++++++++ .../Client/StreamingConstants.cs | 48 +++++++++++ 3 files changed, 177 insertions(+) create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IResponseStream.cs create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingConstants.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IResponseStream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IResponseStream.cs new file mode 100644 index 000000000..6107dde16 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IResponseStream.cs @@ -0,0 +1,85 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// Interface for writing streaming responses in AWS Lambda functions. + /// Obtained by calling ResponseStreamFactory.CreateStream() within a handler. + /// + public interface IResponseStream : IDisposable + { + /// + /// Asynchronously writes a byte array to the response stream. + /// + /// The byte array to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. + /// Thrown if writing would exceed the 20 MiB limit. + Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default); + + /// + /// Asynchronously writes a portion of a byte array to the response stream. + /// + /// The byte array containing data to write. + /// The zero-based byte offset in buffer at which to begin copying bytes. + /// The number of bytes to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. + /// Thrown if writing would exceed the 20 MiB limit. + Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default); + + /// + /// Asynchronously writes a memory buffer to the response stream. + /// + /// The memory buffer to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. + /// Thrown if writing would exceed the 20 MiB limit. + Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default); + + /// + /// Reports an error that occurred during streaming. + /// This will send error information via HTTP trailing headers. + /// + /// The exception to report. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has already been reported. + Task ReportErrorAsync(Exception exception, CancellationToken cancellationToken = default); + + /// + /// Gets the total number of bytes written to the stream so far. + /// + long BytesWritten { get; } + + /// + /// Gets whether the stream has been completed. + /// + bool IsCompleted { get; } + + /// + /// Gets whether an error has been reported. + /// + bool HasError { get; } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs new file mode 100644 index 000000000..fed7352a2 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// Internal context class used by ResponseStreamFactory to track per-invocation streaming state. + /// + internal class ResponseStreamContext + { + /// + /// The AWS request ID for the current invocation. + /// + public string AwsRequestId { get; set; } + + /// + /// Maximum allowed response size in bytes (20 MiB). + /// + public long MaxResponseSize { get; set; } + + /// + /// Whether CreateStream() has been called for this invocation. + /// + public bool StreamCreated { get; set; } + + /// + /// The IResponseStream instance if created. Typed as IResponseStream for now; + /// will be used with the concrete ResponseStream internally. + /// + public IResponseStream Stream { get; set; } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingConstants.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingConstants.cs new file mode 100644 index 000000000..7eeec86a2 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingConstants.cs @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// Constants used for Lambda response streaming. + /// + internal static class StreamingConstants + { + /// + /// Maximum response size for Lambda streaming responses: 20 MiB. + /// + public const long MaxResponseSize = 20 * 1024 * 1024; + + /// + /// Header name for Lambda response mode. + /// + public const string ResponseModeHeader = "Lambda-Runtime-Function-Response-Mode"; + + /// + /// Value for streaming response mode. + /// + public const string StreamingResponseMode = "streaming"; + + /// + /// Trailer header name for error type. + /// + public const string ErrorTypeTrailer = "Lambda-Runtime-Function-Error-Type"; + + /// + /// Trailer header name for error body. + /// + public const string ErrorBodyTrailer = "Lambda-Runtime-Function-Error-Body"; + } +} From 5e0f8101f8b273ac475707f1737c59242cb940f3 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 11 Feb 2026 16:24:58 -0800 Subject: [PATCH 02/13] Task 2 --- .../Client/ResponseStream.cs | 162 ++++++++++++++ .../Client/ResponseStreamContext.cs | 5 +- .../ResponseStreamTests.cs | 209 ++++++++++++++++++ 3 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs new file mode 100644 index 000000000..1484d1f8d --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs @@ -0,0 +1,162 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// Internal implementation of IResponseStream. + /// Buffers written data as chunks for HTTP chunked transfer encoding. + /// + internal class ResponseStream : IResponseStream + { + private readonly long _maxResponseSize; + private readonly List _chunks; + private long _bytesWritten; + private bool _isCompleted; + private bool _hasError; + private Exception _reportedError; + private readonly object _lock = new object(); + + public long BytesWritten => _bytesWritten; + public bool IsCompleted => _isCompleted; + public bool HasError => _hasError; + + internal IReadOnlyList Chunks + { + get + { + lock (_lock) + { + return _chunks.ToList(); + } + } + } + + internal Exception ReportedError => _reportedError; + + public ResponseStream(long maxResponseSize) + { + _maxResponseSize = maxResponseSize; + _chunks = new List(); + _bytesWritten = 0; + _isCompleted = false; + _hasError = false; + } + + public Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + return WriteAsync(buffer, 0, buffer.Length, cancellationToken); + } + + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0 || offset + count > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(count)); + + lock (_lock) + { + ThrowIfCompletedOrError(); + + if (_bytesWritten + count > _maxResponseSize) + { + throw new InvalidOperationException( + $"Writing {count} bytes would exceed the maximum response size of {_maxResponseSize} bytes (20 MiB). " + + $"Current size: {_bytesWritten} bytes."); + } + + var chunk = new byte[count]; + Array.Copy(buffer, offset, chunk, 0, count); + _chunks.Add(chunk); + _bytesWritten += count; + } + + return Task.CompletedTask; + } + + public Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + lock (_lock) + { + ThrowIfCompletedOrError(); + + if (_bytesWritten + buffer.Length > _maxResponseSize) + { + throw new InvalidOperationException( + $"Writing {buffer.Length} bytes would exceed the maximum response size of {_maxResponseSize} bytes (20 MiB). " + + $"Current size: {_bytesWritten} bytes."); + } + + var chunk = buffer.ToArray(); + _chunks.Add(chunk); + _bytesWritten += buffer.Length; + } + + return Task.CompletedTask; + } + + public Task ReportErrorAsync(Exception exception, CancellationToken cancellationToken = default) + { + if (exception == null) + throw new ArgumentNullException(nameof(exception)); + + lock (_lock) + { + if (_isCompleted) + throw new InvalidOperationException("Cannot report an error after the stream has been completed."); + if (_hasError) + throw new InvalidOperationException("An error has already been reported for this stream."); + + _hasError = true; + _reportedError = exception; + } + + return Task.CompletedTask; + } + + internal void MarkCompleted() + { + lock (_lock) + { + _isCompleted = true; + } + } + + private void ThrowIfCompletedOrError() + { + if (_isCompleted) + throw new InvalidOperationException("Cannot write to a completed stream."); + if (_hasError) + throw new InvalidOperationException("Cannot write to a stream after an error has been reported."); + } + + public void Dispose() + { + // Nothing to dispose - all data is in managed memory + } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs index fed7352a2..07df616e3 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs @@ -36,9 +36,8 @@ internal class ResponseStreamContext public bool StreamCreated { get; set; } /// - /// The IResponseStream instance if created. Typed as IResponseStream for now; - /// will be used with the concrete ResponseStream internally. + /// The ResponseStream instance if created. /// - public IResponseStream Stream { get; set; } + public ResponseStream Stream { get; set; } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs new file mode 100644 index 000000000..7503277ca --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs @@ -0,0 +1,209 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + public class ResponseStreamTests + { + private const long MaxResponseSize = 20 * 1024 * 1024; // 20 MiB + + [Fact] + public void Constructor_InitializesStateCorrectly() + { + var stream = new ResponseStream(MaxResponseSize); + + Assert.Equal(0, stream.BytesWritten); + Assert.False(stream.IsCompleted); + Assert.False(stream.HasError); + Assert.Empty(stream.Chunks); + Assert.Null(stream.ReportedError); + } + + [Fact] + public async Task WriteAsync_ByteArray_BuffersDataCorrectly() + { + var stream = new ResponseStream(MaxResponseSize); + var data = new byte[] { 1, 2, 3, 4, 5 }; + + await stream.WriteAsync(data); + + Assert.Equal(5, stream.BytesWritten); + Assert.Single(stream.Chunks); + Assert.Equal(data, stream.Chunks[0]); + } + + [Fact] + public async Task WriteAsync_WithOffset_BuffersCorrectSlice() + { + var stream = new ResponseStream(MaxResponseSize); + var data = new byte[] { 0, 1, 2, 3, 0 }; + + await stream.WriteAsync(data, 1, 3); + + Assert.Equal(3, stream.BytesWritten); + Assert.Equal(new byte[] { 1, 2, 3 }, stream.Chunks[0]); + } + + [Fact] + public async Task WriteAsync_ReadOnlyMemory_BuffersDataCorrectly() + { + var stream = new ResponseStream(MaxResponseSize); + var data = new ReadOnlyMemory(new byte[] { 10, 20, 30 }); + + await stream.WriteAsync(data); + + Assert.Equal(3, stream.BytesWritten); + Assert.Equal(new byte[] { 10, 20, 30 }, stream.Chunks[0]); + } + + [Fact] + public async Task WriteAsync_MultipleWrites_AccumulatesBytesWritten() + { + var stream = new ResponseStream(MaxResponseSize); + + await stream.WriteAsync(new byte[100]); + await stream.WriteAsync(new byte[200]); + await stream.WriteAsync(new byte[300]); + + Assert.Equal(600, stream.BytesWritten); + Assert.Equal(3, stream.Chunks.Count); + } + + [Fact] + public async Task WriteAsync_CopiesData_AvoidingBufferReuseIssues() + { + var stream = new ResponseStream(MaxResponseSize); + var buffer = new byte[] { 1, 2, 3 }; + + await stream.WriteAsync(buffer); + buffer[0] = 99; // mutate original + + Assert.Equal(1, stream.Chunks[0][0]); // chunk should be unaffected + } + + /// + /// Property 6: Size Limit Enforcement - Writing beyond 20 MiB throws InvalidOperationException. + /// Validates: Requirements 3.6, 3.7 + /// + [Theory] + [InlineData(21 * 1024 * 1024)] // Single write exceeding limit + public async Task SizeLimit_SingleWriteExceedingLimit_Throws(int writeSize) + { + var stream = new ResponseStream(MaxResponseSize); + var data = new byte[writeSize]; + + await Assert.ThrowsAsync(() => stream.WriteAsync(data)); + } + + /// + /// Property 6: Size Limit Enforcement - Multiple writes exceeding 20 MiB throws. + /// Validates: Requirements 3.6, 3.7 + /// + [Fact] + public async Task SizeLimit_MultipleWritesExceedingLimit_Throws() + { + var stream = new ResponseStream(MaxResponseSize); + + await stream.WriteAsync(new byte[10 * 1024 * 1024]); + await Assert.ThrowsAsync( + () => stream.WriteAsync(new byte[11 * 1024 * 1024])); + } + + [Fact] + public async Task SizeLimit_ExactlyAtLimit_Succeeds() + { + var stream = new ResponseStream(MaxResponseSize); + var data = new byte[20 * 1024 * 1024]; + + await stream.WriteAsync(data); + + Assert.Equal(MaxResponseSize, stream.BytesWritten); + } + + /// + /// Property 19: Writes After Completion Rejected - Writes after completion throw InvalidOperationException. + /// Validates: Requirements 8.8 + /// + [Fact] + public async Task WriteAsync_AfterMarkCompleted_Throws() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 1 }); + stream.MarkCompleted(); + + await Assert.ThrowsAsync( + () => stream.WriteAsync(new byte[] { 2 })); + } + + [Fact] + public async Task WriteAsync_AfterReportError_Throws() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 1 }); + await stream.ReportErrorAsync(new Exception("test")); + + await Assert.ThrowsAsync( + () => stream.WriteAsync(new byte[] { 2 })); + } + + // --- Error handling tests (2.6) --- + + [Fact] + public async Task ReportErrorAsync_SetsErrorState() + { + var stream = new ResponseStream(MaxResponseSize); + var exception = new InvalidOperationException("something broke"); + + await stream.ReportErrorAsync(exception); + + Assert.True(stream.HasError); + Assert.Same(exception, stream.ReportedError); + } + + [Fact] + public async Task ReportErrorAsync_AfterCompleted_Throws() + { + var stream = new ResponseStream(MaxResponseSize); + stream.MarkCompleted(); + + await Assert.ThrowsAsync( + () => stream.ReportErrorAsync(new Exception("test"))); + } + + [Fact] + public async Task ReportErrorAsync_CalledTwice_Throws() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.ReportErrorAsync(new Exception("first")); + + await Assert.ThrowsAsync( + () => stream.ReportErrorAsync(new Exception("second"))); + } + + [Fact] + public void MarkCompleted_SetsCompletionState() + { + var stream = new ResponseStream(MaxResponseSize); + + stream.MarkCompleted(); + + Assert.True(stream.IsCompleted); + } + } +} From 20e5ba8c50681295cf44cbcc63f5d96852d3ad98 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 11 Feb 2026 16:31:17 -0800 Subject: [PATCH 03/13] Task 3 --- .../Client/ResponseStreamFactory.cs | 109 ++++++++++ .../ResponseStreamFactoryTests.cs | 188 ++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs new file mode 100644 index 000000000..9b60eacfd --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs @@ -0,0 +1,109 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Threading; + +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// Factory for creating streaming responses in AWS Lambda functions. + /// Call CreateStream() within your handler to opt into response streaming for that invocation. + /// + public static class ResponseStreamFactory + { + // For on-demand mode (single invocation at a time) + private static ResponseStreamContext _onDemandContext; + + // For multi-concurrency mode (multiple concurrent invocations) + private static readonly AsyncLocal _asyncLocalContext = new AsyncLocal(); + + /// + /// Creates a streaming response for the current invocation. + /// Can only be called once per invocation. + /// + /// An IResponseStream for writing response data. + /// Thrown if called outside an invocation context. + /// Thrown if called more than once per invocation. + public static IResponseStream CreateStream() + { + var context = GetCurrentContext(); + + if (context == null) + { + throw new InvalidOperationException( + "ResponseStreamFactory.CreateStream() can only be called within a Lambda handler invocation."); + } + + if (context.StreamCreated) + { + throw new InvalidOperationException( + "ResponseStreamFactory.CreateStream() can only be called once per invocation."); + } + + var stream = new ResponseStream(context.MaxResponseSize); + context.Stream = stream; + context.StreamCreated = true; + + return stream; + } + + // Internal methods for LambdaBootstrap to manage state + + internal static void InitializeInvocation(string awsRequestId, long maxResponseSize, bool isMultiConcurrency) + { + var context = new ResponseStreamContext + { + AwsRequestId = awsRequestId, + MaxResponseSize = maxResponseSize, + StreamCreated = false, + Stream = null + }; + + if (isMultiConcurrency) + { + _asyncLocalContext.Value = context; + } + else + { + _onDemandContext = context; + } + } + + internal static ResponseStream GetStreamIfCreated(bool isMultiConcurrency) + { + var context = isMultiConcurrency ? _asyncLocalContext.Value : _onDemandContext; + return context?.Stream; + } + + internal static void CleanupInvocation(bool isMultiConcurrency) + { + if (isMultiConcurrency) + { + _asyncLocalContext.Value = null; + } + else + { + _onDemandContext = null; + } + } + + private static ResponseStreamContext GetCurrentContext() + { + // Check multi-concurrency first (AsyncLocal), then on-demand + return _asyncLocalContext.Value ?? _onDemandContext; + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs new file mode 100644 index 000000000..a4b0558af --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs @@ -0,0 +1,188 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + public class ResponseStreamFactoryTests : IDisposable + { + private const long MaxResponseSize = 20 * 1024 * 1024; + + public void Dispose() + { + // Clean up both modes to avoid test pollution + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + } + + // --- Task 3.3: CreateStream tests --- + + /// + /// Property 1: CreateStream Returns Valid Stream - on-demand mode. + /// Validates: Requirements 1.3, 2.2, 2.3 + /// + [Fact] + public void CreateStream_OnDemandMode_ReturnsValidStream() + { + ResponseStreamFactory.InitializeInvocation("req-1", MaxResponseSize, isMultiConcurrency: false); + + var stream = ResponseStreamFactory.CreateStream(); + + Assert.NotNull(stream); + Assert.IsAssignableFrom(stream); + } + + /// + /// Property 1: CreateStream Returns Valid Stream - multi-concurrency mode. + /// Validates: Requirements 1.3, 2.2, 2.3 + /// + [Fact] + public void CreateStream_MultiConcurrencyMode_ReturnsValidStream() + { + ResponseStreamFactory.InitializeInvocation("req-2", MaxResponseSize, isMultiConcurrency: true); + + var stream = ResponseStreamFactory.CreateStream(); + + Assert.NotNull(stream); + Assert.IsAssignableFrom(stream); + } + + /// + /// Property 4: Single Stream Per Invocation - calling CreateStream twice throws. + /// Validates: Requirements 2.5, 2.6 + /// + [Fact] + public void CreateStream_CalledTwice_ThrowsInvalidOperationException() + { + ResponseStreamFactory.InitializeInvocation("req-3", MaxResponseSize, isMultiConcurrency: false); + ResponseStreamFactory.CreateStream(); + + Assert.Throws(() => ResponseStreamFactory.CreateStream()); + } + + [Fact] + public void CreateStream_OutsideInvocationContext_ThrowsInvalidOperationException() + { + // No InitializeInvocation called + Assert.Throws(() => ResponseStreamFactory.CreateStream()); + } + + // --- Task 3.5: Internal methods tests --- + + [Fact] + public void InitializeInvocation_OnDemand_SetsUpContext() + { + ResponseStreamFactory.InitializeInvocation("req-4", MaxResponseSize, isMultiConcurrency: false); + + // GetStreamIfCreated should return null since CreateStream hasn't been called + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + + // But CreateStream should work (proving context was set up) + var stream = ResponseStreamFactory.CreateStream(); + Assert.NotNull(stream); + } + + [Fact] + public void InitializeInvocation_MultiConcurrency_SetsUpContext() + { + ResponseStreamFactory.InitializeInvocation("req-5", MaxResponseSize, isMultiConcurrency: true); + + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true)); + + var stream = ResponseStreamFactory.CreateStream(); + Assert.NotNull(stream); + } + + [Fact] + public void GetStreamIfCreated_AfterCreateStream_ReturnsStream() + { + ResponseStreamFactory.InitializeInvocation("req-6", MaxResponseSize, isMultiConcurrency: false); + var created = ResponseStreamFactory.CreateStream(); + + var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false); + + Assert.NotNull(retrieved); + } + + [Fact] + public void GetStreamIfCreated_NoContext_ReturnsNull() + { + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + } + + [Fact] + public void CleanupInvocation_ClearsState() + { + ResponseStreamFactory.InitializeInvocation("req-7", MaxResponseSize, isMultiConcurrency: false); + ResponseStreamFactory.CreateStream(); + + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + Assert.Throws(() => ResponseStreamFactory.CreateStream()); + } + + /// + /// Property 16: State Isolation Between Invocations - state from one invocation doesn't leak to the next. + /// Validates: Requirements 6.5, 8.9 + /// + [Fact] + public void StateIsolation_SequentialInvocations_NoLeakage() + { + // First invocation - streaming + ResponseStreamFactory.InitializeInvocation("req-8a", MaxResponseSize, isMultiConcurrency: false); + var stream1 = ResponseStreamFactory.CreateStream(); + Assert.NotNull(stream1); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + + // Second invocation - should start fresh + ResponseStreamFactory.InitializeInvocation("req-8b", MaxResponseSize, isMultiConcurrency: false); + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + + // Should be able to create a new stream + var stream2 = ResponseStreamFactory.CreateStream(); + Assert.NotNull(stream2); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + } + + /// + /// Property 16: State Isolation - multi-concurrency mode uses AsyncLocal. + /// Validates: Requirements 2.9, 2.10 + /// + [Fact] + public async Task StateIsolation_MultiConcurrency_UsesAsyncLocal() + { + // Initialize in multi-concurrency mode on main thread + ResponseStreamFactory.InitializeInvocation("req-9", MaxResponseSize, isMultiConcurrency: true); + var stream = ResponseStreamFactory.CreateStream(); + Assert.NotNull(stream); + + // A separate task should not see the main thread's context + // (AsyncLocal flows to child tasks, but a fresh Task.Run with new initialization should override) + bool childSawNull = false; + await Task.Run(() => + { + // Clean up the flowed context first + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + childSawNull = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true) == null; + }); + + Assert.True(childSawNull); + } + } +} From 0cdb15916b475b92379c84cab06da4c6383b9a61 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 16 Feb 2026 11:51:17 -0800 Subject: [PATCH 04/13] Task 4 --- .../Client/InvocationResponse.cs | 26 ++++++ .../InvocationResponseTests.cs | 81 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/InvocationResponseTests.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationResponse.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationResponse.cs index 1894b0521..4438c9708 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationResponse.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationResponse.cs @@ -34,6 +34,18 @@ public class InvocationResponse /// public bool DisposeOutputStream { get; private set; } = true; + /// + /// Indicates whether this response uses streaming mode. + /// Set internally by the runtime when ResponseStreamFactory.CreateStream() is called. + /// + internal bool IsStreaming { get; set; } + + /// + /// The ResponseStream instance if streaming mode is used. + /// Set internally by the runtime. + /// + internal ResponseStream ResponseStream { get; set; } + /// /// Construct a InvocationResponse with an output stream that will be disposed by the Lambda Runtime Client. /// @@ -52,6 +64,20 @@ public InvocationResponse(Stream outputStream, bool disposeOutputStream) { OutputStream = outputStream ?? throw new ArgumentNullException(nameof(outputStream)); DisposeOutputStream = disposeOutputStream; + IsStreaming = false; + } + + /// + /// Creates an InvocationResponse for a streaming response. + /// Used internally by the runtime. + /// + internal static InvocationResponse CreateStreamingResponse(ResponseStream responseStream) + { + return new InvocationResponse(Stream.Null, false) + { + IsStreaming = true, + ResponseStream = responseStream + }; } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/InvocationResponseTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/InvocationResponseTests.cs new file mode 100644 index 000000000..703ac0cd9 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/InvocationResponseTests.cs @@ -0,0 +1,81 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.IO; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + public class InvocationResponseTests + { + private const long MaxResponseSize = 20 * 1024 * 1024; + + /// + /// Property 17: InvocationResponse Streaming Flag - Existing constructors set IsStreaming to false. + /// Validates: Requirements 7.3, 8.1 + /// + [Fact] + public void Constructor_WithStream_IsStreamingIsFalse() + { + var response = new InvocationResponse(new MemoryStream()); + + Assert.False(response.IsStreaming); + Assert.Null(response.ResponseStream); + } + + [Fact] + public void Constructor_WithStreamAndDispose_IsStreamingIsFalse() + { + var response = new InvocationResponse(new MemoryStream(), false); + + Assert.False(response.IsStreaming); + Assert.Null(response.ResponseStream); + } + + /// + /// Property 17: InvocationResponse Streaming Flag - CreateStreamingResponse sets IsStreaming to true. + /// Validates: Requirements 7.3, 8.1 + /// + [Fact] + public void CreateStreamingResponse_SetsIsStreamingTrue() + { + var stream = new ResponseStream(MaxResponseSize); + + var response = InvocationResponse.CreateStreamingResponse(stream); + + Assert.True(response.IsStreaming); + } + + [Fact] + public void CreateStreamingResponse_SetsResponseStream() + { + var stream = new ResponseStream(MaxResponseSize); + + var response = InvocationResponse.CreateStreamingResponse(stream); + + Assert.Same(stream, response.ResponseStream); + } + + [Fact] + public void CreateStreamingResponse_DoesNotDisposeOutputStream() + { + var stream = new ResponseStream(MaxResponseSize); + + var response = InvocationResponse.CreateStreamingResponse(stream); + + Assert.False(response.DisposeOutputStream); + } + } +} From 0aa892be011d2f10b774e3ff7cf1eaedbc2669ea Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 16 Feb 2026 11:58:24 -0800 Subject: [PATCH 05/13] Task 5 --- .../Client/StreamingHttpContent.cs | 95 +++++++ .../StreamingHttpContentTests.cs | 264 ++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs new file mode 100644 index 000000000..c853ed5dd --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs @@ -0,0 +1,95 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// HttpContent implementation for streaming responses with chunked transfer encoding. + /// + internal class StreamingHttpContent : HttpContent + { + private static readonly byte[] CrlfBytes = Encoding.ASCII.GetBytes("\r\n"); + private static readonly byte[] FinalChunkBytes = Encoding.ASCII.GetBytes("0\r\n"); + + private readonly ResponseStream _responseStream; + + public StreamingHttpContent(ResponseStream responseStream) + { + _responseStream = responseStream ?? throw new ArgumentNullException(nameof(responseStream)); + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + foreach (var chunk in _responseStream.Chunks) + { + await WriteChunkAsync(stream, chunk); + } + + await WriteFinalChunkAsync(stream); + + if (_responseStream.HasError) + { + await WriteErrorTrailersAsync(stream, _responseStream.ReportedError); + } + + await stream.FlushAsync(); + } + + protected override bool TryComputeLength(out long length) + { + length = -1; + return false; + } + + private async Task WriteChunkAsync(Stream stream, byte[] data) + { + var chunkSizeHex = data.Length.ToString("X"); + var chunkSizeBytes = Encoding.ASCII.GetBytes(chunkSizeHex); + + await stream.WriteAsync(chunkSizeBytes, 0, chunkSizeBytes.Length); + await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length); + await stream.WriteAsync(data, 0, data.Length); + await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length); + } + + private async Task WriteFinalChunkAsync(Stream stream) + { + await stream.WriteAsync(FinalChunkBytes, 0, FinalChunkBytes.Length); + } + + private async Task WriteErrorTrailersAsync(Stream stream, Exception exception) + { + var exceptionInfo = ExceptionInfo.GetExceptionInfo(exception); + + var errorTypeHeader = $"{StreamingConstants.ErrorTypeTrailer}: {exceptionInfo.ErrorType}\r\n"; + var errorTypeBytes = Encoding.UTF8.GetBytes(errorTypeHeader); + await stream.WriteAsync(errorTypeBytes, 0, errorTypeBytes.Length); + + var errorBodyJson = LambdaJsonExceptionWriter.WriteJson(exceptionInfo); + var errorBodyHeader = $"{StreamingConstants.ErrorBodyTrailer}: {errorBodyJson}\r\n"; + var errorBodyBytes = Encoding.UTF8.GetBytes(errorBodyHeader); + await stream.WriteAsync(errorBodyBytes, 0, errorBodyBytes.Length); + + await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs new file mode 100644 index 000000000..0682a816e --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs @@ -0,0 +1,264 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + public class StreamingHttpContentTests + { + private const long MaxResponseSize = 20 * 1024 * 1024; + + private async Task SerializeContentAsync(StreamingHttpContent content) + { + using var ms = new MemoryStream(); + await content.CopyToAsync(ms); + return ms.ToArray(); + } + + // --- Task 5.4: Chunked encoding format tests --- + + /// + /// Property 9: Chunked Encoding Format - chunks are formatted as size(hex) + CRLF + data + CRLF. + /// Validates: Requirements 4.3, 10.1, 10.2 + /// + [Theory] + [InlineData(1)] + [InlineData(10)] + [InlineData(255)] + [InlineData(4096)] + public async Task ChunkedEncoding_SingleChunk_CorrectFormat(int chunkSize) + { + var stream = new ResponseStream(MaxResponseSize); + var data = new byte[chunkSize]; + for (int i = 0; i < data.Length; i++) data[i] = (byte)(i % 256); + await stream.WriteAsync(data); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + var outputStr = Encoding.ASCII.GetString(output); + + var expectedSizeHex = chunkSize.ToString("X"); + Assert.StartsWith(expectedSizeHex + "\r\n", outputStr); + + // Verify chunk data follows the size line + var dataStart = expectedSizeHex.Length + 2; // size + CRLF + for (int i = 0; i < chunkSize; i++) + { + Assert.Equal(data[i], output[dataStart + i]); + } + + // Verify CRLF after data + Assert.Equal((byte)'\r', output[dataStart + chunkSize]); + Assert.Equal((byte)'\n', output[dataStart + chunkSize + 1]); + } + + /// + /// Property 9: Chunked Encoding Format - multiple chunks each formatted correctly. + /// Validates: Requirements 4.3, 10.1 + /// + [Fact] + public async Task ChunkedEncoding_MultipleChunks_EachFormattedCorrectly() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 0xAA, 0xBB }); + await stream.WriteAsync(new byte[] { 0xCC }); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + var outputStr = Encoding.ASCII.GetString(output); + + // First chunk: "2\r\n" + 2 bytes + "\r\n" + Assert.StartsWith("2\r\n", outputStr); + Assert.Equal(0xAA, output[3]); + Assert.Equal(0xBB, output[4]); + Assert.Equal((byte)'\r', output[5]); + Assert.Equal((byte)'\n', output[6]); + + // Second chunk: "1\r\n" + 1 byte + "\r\n" + Assert.Equal((byte)'1', output[7]); + Assert.Equal((byte)'\r', output[8]); + Assert.Equal((byte)'\n', output[9]); + Assert.Equal(0xCC, output[10]); + Assert.Equal((byte)'\r', output[11]); + Assert.Equal((byte)'\n', output[12]); + } + + /// + /// Property 20: Final Chunk Termination - final chunk "0\r\n" is written. + /// Validates: Requirements 10.2, 10.5 + /// + [Fact] + public async Task FinalChunk_IsWritten() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 1 }); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + var outputStr = Encoding.ASCII.GetString(output); + + // Output should end with final chunk "0\r\n" + Assert.EndsWith("0\r\n", outputStr); + } + + [Fact] + public async Task FinalChunk_EmptyStream_OnlyFinalChunk() + { + var stream = new ResponseStream(MaxResponseSize); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + + Assert.Equal(Encoding.ASCII.GetBytes("0\r\n"), output); + } + + /// + /// Property 22: CRLF Line Terminators - all line terminators are CRLF, not just LF. + /// Validates: Requirements 10.5 + /// + [Fact] + public async Task CrlfTerminators_NoBareLineFeed() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 65, 66, 67 }); // "ABC" + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + + // Check every \n is preceded by \r + for (int i = 0; i < output.Length; i++) + { + if (output[i] == (byte)'\n') + { + Assert.True(i > 0 && output[i - 1] == (byte)'\r', + $"Found bare LF at position {i} without preceding CR"); + } + } + } + + [Fact] + public void TryComputeLength_ReturnsFalse() + { + var stream = new ResponseStream(MaxResponseSize); + var content = new StreamingHttpContent(stream); + + var result = content.Headers.ContentLength; + + Assert.Null(result); + } + + // --- Task 5.6: Error trailer tests --- + + /// + /// Property 11: Midstream Error Type Trailer - error type trailer is included for various exception types. + /// Validates: Requirements 5.1, 5.2 + /// + [Theory] + [InlineData(typeof(InvalidOperationException))] + [InlineData(typeof(ArgumentException))] + [InlineData(typeof(NullReferenceException))] + public async Task ErrorTrailer_IncludesErrorType(Type exceptionType) + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 1 }); + var exception = (Exception)Activator.CreateInstance(exceptionType, "test error"); + await stream.ReportErrorAsync(exception); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + var outputStr = Encoding.UTF8.GetString(output); + + Assert.Contains($"Lambda-Runtime-Function-Error-Type: {exceptionType.Name}", outputStr); + } + + /// + /// Property 12: Midstream Error Body Trailer - error body trailer includes JSON exception details. + /// Validates: Requirements 5.3 + /// + [Fact] + public async Task ErrorTrailer_IncludesJsonErrorBody() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 1 }); + await stream.ReportErrorAsync(new InvalidOperationException("something went wrong")); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + var outputStr = Encoding.UTF8.GetString(output); + + Assert.Contains("Lambda-Runtime-Function-Error-Body:", outputStr); + Assert.Contains("something went wrong", outputStr); + Assert.Contains("InvalidOperationException", outputStr); + } + + /// + /// Property 21: Trailer Ordering - trailers appear after final chunk. + /// Validates: Requirements 10.3 + /// + [Fact] + public async Task ErrorTrailers_AppearAfterFinalChunk() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 1 }); + await stream.ReportErrorAsync(new Exception("fail")); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + var outputStr = Encoding.UTF8.GetString(output); + + var finalChunkIndex = outputStr.IndexOf("0\r\n"); + var errorTypeIndex = outputStr.IndexOf("Lambda-Runtime-Function-Error-Type:"); + var errorBodyIndex = outputStr.IndexOf("Lambda-Runtime-Function-Error-Body:"); + + Assert.True(finalChunkIndex >= 0, "Final chunk not found"); + Assert.True(errorTypeIndex > finalChunkIndex, "Error type trailer should appear after final chunk"); + Assert.True(errorBodyIndex > finalChunkIndex, "Error body trailer should appear after final chunk"); + } + + [Fact] + public async Task NoError_NoTrailersWritten() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 1 }); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + var outputStr = Encoding.UTF8.GetString(output); + + Assert.DoesNotContain("Lambda-Runtime-Function-Error-Type:", outputStr); + Assert.DoesNotContain("Lambda-Runtime-Function-Error-Body:", outputStr); + } + + [Fact] + public async Task ErrorTrailers_EndWithCrlf() + { + var stream = new ResponseStream(MaxResponseSize); + await stream.WriteAsync(new byte[] { 1 }); + await stream.ReportErrorAsync(new Exception("fail")); + + var content = new StreamingHttpContent(stream); + var output = await SerializeContentAsync(content); + var outputStr = Encoding.UTF8.GetString(output); + + // Should end with final CRLF after trailers + Assert.EndsWith("\r\n", outputStr); + } + } +} From 5e29c2141e59813b05ecc93578badd2a13e8227c Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 16 Feb 2026 18:05:11 -0800 Subject: [PATCH 06/13] Task 4 (after redesign) --- .../Client/ResponseStream.cs | 123 +++--- .../Client/ResponseStreamContext.cs | 19 + .../Client/StreamingHttpContent.cs | 34 +- .../ResponseStreamTests.cs | 268 +++++++++++-- .../StreamingHttpContentTests.cs | 353 +++++++++++------- 5 files changed, 546 insertions(+), 251 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs index 1484d1f8d..00f63cf75 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs @@ -14,62 +14,70 @@ */ using System; -using System.Collections.Generic; -using System.Linq; +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; namespace Amazon.Lambda.RuntimeSupport { /// - /// Internal implementation of IResponseStream. - /// Buffers written data as chunks for HTTP chunked transfer encoding. + /// Internal implementation of IResponseStream with true streaming. + /// Writes data directly to the HTTP output stream as chunked transfer encoding. /// internal class ResponseStream : IResponseStream { + private static readonly byte[] CrlfBytes = Encoding.ASCII.GetBytes("\r\n"); + private readonly long _maxResponseSize; - private readonly List _chunks; private long _bytesWritten; private bool _isCompleted; private bool _hasError; private Exception _reportedError; private readonly object _lock = new object(); + // The live HTTP output stream, set by StreamingHttpContent when SerializeToStreamAsync is called. + private Stream _httpOutputStream; + private readonly SemaphoreSlim _httpStreamReady = new SemaphoreSlim(0, 1); + private readonly SemaphoreSlim _completionSignal = new SemaphoreSlim(0, 1); + public long BytesWritten => _bytesWritten; public bool IsCompleted => _isCompleted; public bool HasError => _hasError; + internal Exception ReportedError => _reportedError; - internal IReadOnlyList Chunks + public ResponseStream(long maxResponseSize) { - get - { - lock (_lock) - { - return _chunks.ToList(); - } - } + _maxResponseSize = maxResponseSize; } - internal Exception ReportedError => _reportedError; + /// + /// Called by StreamingHttpContent.SerializeToStreamAsync to provide the HTTP output stream. + /// + internal void SetHttpOutputStream(Stream httpOutputStream) + { + _httpOutputStream = httpOutputStream; + _httpStreamReady.Release(); + } - public ResponseStream(long maxResponseSize) + /// + /// Called by StreamingHttpContent.SerializeToStreamAsync to wait until the handler + /// finishes writing (MarkCompleted or ReportErrorAsync). + /// + internal async Task WaitForCompletionAsync() { - _maxResponseSize = maxResponseSize; - _chunks = new List(); - _bytesWritten = 0; - _isCompleted = false; - _hasError = false; + await _completionSignal.WaitAsync(); } - public Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default) + public async Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); - return WriteAsync(buffer, 0, buffer.Length, cancellationToken); + await WriteAsync(buffer, 0, buffer.Length, cancellationToken); } - public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); @@ -78,45 +86,45 @@ public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken c if (count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException(nameof(count)); - lock (_lock) + // Wait for the HTTP stream to be ready (first write only blocks) + await _httpStreamReady.WaitAsync(cancellationToken); + try { - ThrowIfCompletedOrError(); - - if (_bytesWritten + count > _maxResponseSize) + lock (_lock) { - throw new InvalidOperationException( - $"Writing {count} bytes would exceed the maximum response size of {_maxResponseSize} bytes (20 MiB). " + - $"Current size: {_bytesWritten} bytes."); - } - - var chunk = new byte[count]; - Array.Copy(buffer, offset, chunk, 0, count); - _chunks.Add(chunk); - _bytesWritten += count; - } + ThrowIfCompletedOrError(); - return Task.CompletedTask; - } - - public Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - lock (_lock) - { - ThrowIfCompletedOrError(); + if (_bytesWritten + count > _maxResponseSize) + { + throw new InvalidOperationException( + $"Writing {count} bytes would exceed the maximum response size of {_maxResponseSize} bytes (20 MiB). " + + $"Current size: {_bytesWritten} bytes."); + } - if (_bytesWritten + buffer.Length > _maxResponseSize) - { - throw new InvalidOperationException( - $"Writing {buffer.Length} bytes would exceed the maximum response size of {_maxResponseSize} bytes (20 MiB). " + - $"Current size: {_bytesWritten} bytes."); + _bytesWritten += count; } - var chunk = buffer.ToArray(); - _chunks.Add(chunk); - _bytesWritten += buffer.Length; + // Write chunk directly to the HTTP stream: size(hex) + CRLF + data + CRLF + var chunkSizeHex = count.ToString("X"); + var chunkSizeBytes = Encoding.ASCII.GetBytes(chunkSizeHex); + await _httpOutputStream.WriteAsync(chunkSizeBytes, 0, chunkSizeBytes.Length, cancellationToken); + await _httpOutputStream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length, cancellationToken); + await _httpOutputStream.WriteAsync(buffer, offset, count, cancellationToken); + await _httpOutputStream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length, cancellationToken); + await _httpOutputStream.FlushAsync(cancellationToken); + } + finally + { + // Re-release so subsequent writes don't block + _httpStreamReady.Release(); } + } - return Task.CompletedTask; + public async Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + // Convert to array and delegate — small overhead but keeps the API simple + var array = buffer.ToArray(); + await WriteAsync(array, 0, array.Length, cancellationToken); } public Task ReportErrorAsync(Exception exception, CancellationToken cancellationToken = default) @@ -135,6 +143,8 @@ public Task ReportErrorAsync(Exception exception, CancellationToken cancellation _reportedError = exception; } + // Signal completion so StreamingHttpContent can write error trailers and finish + _completionSignal.Release(); return Task.CompletedTask; } @@ -144,6 +154,8 @@ internal void MarkCompleted() { _isCompleted = true; } + // Signal completion so StreamingHttpContent can write the final chunk and finish + _completionSignal.Release(); } private void ThrowIfCompletedOrError() @@ -156,7 +168,8 @@ private void ThrowIfCompletedOrError() public void Dispose() { - // Nothing to dispose - all data is in managed memory + // Ensure completion is signaled if not already + try { _completionSignal.Release(); } catch (SemaphoreFullException) { } } } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs index 07df616e3..dc0b4a629 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs @@ -13,6 +13,9 @@ * permissions and limitations under the License. */ +using System.Threading; +using System.Threading.Tasks; + namespace Amazon.Lambda.RuntimeSupport { /// @@ -39,5 +42,21 @@ internal class ResponseStreamContext /// The ResponseStream instance if created. /// public ResponseStream Stream { get; set; } + + /// + /// The RuntimeApiClient used to start the streaming HTTP POST. + /// + public RuntimeApiClient RuntimeApiClient { get; set; } + + /// + /// Cancellation token for the current invocation. + /// + public CancellationToken CancellationToken { get; set; } + + /// + /// The Task representing the in-flight HTTP POST to the Runtime API. + /// Started when CreateStream() is called, completes when the stream is finalized. + /// + public Task SendTask { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs index c853ed5dd..e563d343b 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs @@ -39,18 +39,24 @@ public StreamingHttpContent(ResponseStream responseStream) protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { - foreach (var chunk in _responseStream.Chunks) - { - await WriteChunkAsync(stream, chunk); - } + // Hand the HTTP output stream to ResponseStream so WriteAsync calls + // can write chunks directly to it. + _responseStream.SetHttpOutputStream(stream); - await WriteFinalChunkAsync(stream); + // Wait for the handler to finish writing (MarkCompleted or ReportErrorAsync) + await _responseStream.WaitForCompletionAsync(); + // Write final chunk + await stream.WriteAsync(FinalChunkBytes, 0, FinalChunkBytes.Length); + + // Write error trailers if present if (_responseStream.HasError) { await WriteErrorTrailersAsync(stream, _responseStream.ReportedError); } + // Write final CRLF to end the chunked message + await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length); await stream.FlushAsync(); } @@ -60,22 +66,6 @@ protected override bool TryComputeLength(out long length) return false; } - private async Task WriteChunkAsync(Stream stream, byte[] data) - { - var chunkSizeHex = data.Length.ToString("X"); - var chunkSizeBytes = Encoding.ASCII.GetBytes(chunkSizeHex); - - await stream.WriteAsync(chunkSizeBytes, 0, chunkSizeBytes.Length); - await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length); - await stream.WriteAsync(data, 0, data.Length); - await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length); - } - - private async Task WriteFinalChunkAsync(Stream stream) - { - await stream.WriteAsync(FinalChunkBytes, 0, FinalChunkBytes.Length); - } - private async Task WriteErrorTrailersAsync(Stream stream, Exception exception) { var exceptionInfo = ExceptionInfo.GetExceptionInfo(exception); @@ -88,8 +78,6 @@ private async Task WriteErrorTrailersAsync(Stream stream, Exception exception) var errorBodyHeader = $"{StreamingConstants.ErrorBodyTrailer}: {errorBodyJson}\r\n"; var errorBodyBytes = Encoding.UTF8.GetBytes(errorBodyHeader); await stream.WriteAsync(errorBodyBytes, 0, errorBodyBytes.Length); - - await stream.WriteAsync(CrlfBytes, 0, CrlfBytes.Length); } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs index 7503277ca..a6ef2fe6f 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs @@ -14,6 +14,10 @@ */ using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -23,6 +27,20 @@ public class ResponseStreamTests { private const long MaxResponseSize = 20 * 1024 * 1024; // 20 MiB + /// + /// Helper: creates a ResponseStream and wires up a MemoryStream as the HTTP output stream. + /// Returns both so tests can inspect what was written. + /// + private static (ResponseStream stream, MemoryStream httpOutput) CreateWiredStream(long maxSize = MaxResponseSize) + { + var rs = new ResponseStream(maxSize); + var output = new MemoryStream(); + rs.SetHttpOutputStream(output); + return (rs, output); + } + + // ---- Basic state tests ---- + [Fact] public void Constructor_InitializesStateCorrectly() { @@ -31,94 +49,220 @@ public void Constructor_InitializesStateCorrectly() Assert.Equal(0, stream.BytesWritten); Assert.False(stream.IsCompleted); Assert.False(stream.HasError); - Assert.Empty(stream.Chunks); Assert.Null(stream.ReportedError); } - [Fact] - public async Task WriteAsync_ByteArray_BuffersDataCorrectly() + // ---- Chunked encoding format (Property 9, Property 22) ---- + + /// + /// Property 9: Chunked Encoding Format — each chunk is hex-size + CRLF + data + CRLF. + /// Property 22: CRLF Line Terminators — all line terminators are \r\n. + /// Validates: Requirements 3.2, 10.1, 10.5 + /// + [Theory] + [InlineData(new byte[] { 1, 2, 3 }, "3")] // 3 bytes → "3" + [InlineData(new byte[] { 0xFF }, "1")] // 1 byte → "1" + [InlineData(new byte[0], "0")] // 0 bytes → "0" + public async Task WriteAsync_WritesChunkedEncodingFormat(byte[] data, string expectedHexSize) { - var stream = new ResponseStream(MaxResponseSize); - var data = new byte[] { 1, 2, 3, 4, 5 }; + var (stream, httpOutput) = CreateWiredStream(); await stream.WriteAsync(data); - Assert.Equal(5, stream.BytesWritten); - Assert.Single(stream.Chunks); - Assert.Equal(data, stream.Chunks[0]); + var written = httpOutput.ToArray(); + var expected = Encoding.ASCII.GetBytes(expectedHexSize + "\r\n") + .Concat(data) + .Concat(Encoding.ASCII.GetBytes("\r\n")) + .ToArray(); + + Assert.Equal(expected, written); } + /// + /// Property 9: Chunked Encoding Format — verify with offset/count overload. + /// Validates: Requirements 3.2, 10.1 + /// [Fact] - public async Task WriteAsync_WithOffset_BuffersCorrectSlice() + public async Task WriteAsync_WithOffset_WritesCorrectSliceAsChunk() { - var stream = new ResponseStream(MaxResponseSize); + var (stream, httpOutput) = CreateWiredStream(); var data = new byte[] { 0, 1, 2, 3, 0 }; await stream.WriteAsync(data, 1, 3); - Assert.Equal(3, stream.BytesWritten); - Assert.Equal(new byte[] { 1, 2, 3 }, stream.Chunks[0]); + var written = httpOutput.ToArray(); + // 3 bytes → hex "3", data is {1,2,3} + var expected = Encoding.ASCII.GetBytes("3\r\n") + .Concat(new byte[] { 1, 2, 3 }) + .Concat(Encoding.ASCII.GetBytes("\r\n")) + .ToArray(); + + Assert.Equal(expected, written); } + /// + /// Property 9: Chunked Encoding Format — ReadOnlyMemory overload. + /// Validates: Requirements 3.2, 10.1 + /// [Fact] - public async Task WriteAsync_ReadOnlyMemory_BuffersDataCorrectly() + public async Task WriteAsync_ReadOnlyMemory_WritesChunkedFormat() { - var stream = new ResponseStream(MaxResponseSize); + var (stream, httpOutput) = CreateWiredStream(); var data = new ReadOnlyMemory(new byte[] { 10, 20, 30 }); await stream.WriteAsync(data); + var written = httpOutput.ToArray(); + var expected = Encoding.ASCII.GetBytes("3\r\n") + .Concat(new byte[] { 10, 20, 30 }) + .Concat(Encoding.ASCII.GetBytes("\r\n")) + .ToArray(); + + Assert.Equal(expected, written); + } + + // ---- Property 5: Written Data Appears in HTTP Response Immediately ---- + + /// + /// Property 5: Written Data Appears in HTTP Response Immediately — + /// each WriteAsync call writes to the HTTP stream before returning. + /// Validates: Requirements 3.2 + /// + [Fact] + public async Task WriteAsync_MultipleWrites_EachAppearsImmediately() + { + var (stream, httpOutput) = CreateWiredStream(); + + await stream.WriteAsync(new byte[] { 0xAA }); + var afterFirst = httpOutput.ToArray().Length; + Assert.True(afterFirst > 0, "First chunk should be on the HTTP stream immediately after WriteAsync returns"); + + await stream.WriteAsync(new byte[] { 0xBB, 0xCC }); + var afterSecond = httpOutput.ToArray().Length; + Assert.True(afterSecond > afterFirst, "Second chunk should appear on the HTTP stream immediately"); + Assert.Equal(3, stream.BytesWritten); - Assert.Equal(new byte[] { 10, 20, 30 }, stream.Chunks[0]); } + /// + /// Property 5: Written Data Appears in HTTP Response Immediately — + /// verify with a larger payload that hex size is multi-character. + /// Validates: Requirements 3.2 + /// [Fact] - public async Task WriteAsync_MultipleWrites_AccumulatesBytesWritten() + public async Task WriteAsync_LargerPayload_HexSizeIsCorrect() { - var stream = new ResponseStream(MaxResponseSize); + var (stream, httpOutput) = CreateWiredStream(); + var data = new byte[256]; // 0x100 + + await stream.WriteAsync(data); - await stream.WriteAsync(new byte[100]); - await stream.WriteAsync(new byte[200]); - await stream.WriteAsync(new byte[300]); + var written = Encoding.ASCII.GetString(httpOutput.ToArray()); + Assert.StartsWith("100\r\n", written); + } + + // ---- Semaphore coordination: _httpStreamReady blocks until SetHttpOutputStream ---- - Assert.Equal(600, stream.BytesWritten); - Assert.Equal(3, stream.Chunks.Count); + /// + /// Test that WriteAsync blocks until SetHttpOutputStream is called. + /// Validates: Requirements 3.2, 10.1 + /// + [Fact] + public async Task WriteAsync_BlocksUntilSetHttpOutputStream() + { + var rs = new ResponseStream(MaxResponseSize); + var httpOutput = new MemoryStream(); + var writeStarted = new ManualResetEventSlim(false); + var writeCompleted = new ManualResetEventSlim(false); + + // Start a write on a background thread — it should block + var writeTask = Task.Run(async () => + { + writeStarted.Set(); + await rs.WriteAsync(new byte[] { 1, 2, 3 }); + writeCompleted.Set(); + }); + + // Wait for the write to start, then verify it hasn't completed + writeStarted.Wait(TimeSpan.FromSeconds(2)); + await Task.Delay(100); // give it a moment + Assert.False(writeCompleted.IsSet, "WriteAsync should block until SetHttpOutputStream is called"); + + // Now provide the HTTP stream — the write should complete + rs.SetHttpOutputStream(httpOutput); + await writeTask; + + Assert.True(writeCompleted.IsSet); + Assert.True(httpOutput.ToArray().Length > 0); } + // ---- Completion signaling: MarkCompleted releases _completionSignal ---- + + /// + /// Test that MarkCompleted releases the completion signal (WaitForCompletionAsync unblocks). + /// Validates: Requirements 5.5, 8.3 + /// [Fact] - public async Task WriteAsync_CopiesData_AvoidingBufferReuseIssues() + public async Task MarkCompleted_ReleasesCompletionSignal() { - var stream = new ResponseStream(MaxResponseSize); - var buffer = new byte[] { 1, 2, 3 }; + var (stream, _) = CreateWiredStream(); + + var waitTask = stream.WaitForCompletionAsync(); + Assert.False(waitTask.IsCompleted, "WaitForCompletionAsync should block before MarkCompleted"); - await stream.WriteAsync(buffer); - buffer[0] = 99; // mutate original + stream.MarkCompleted(); - Assert.Equal(1, stream.Chunks[0][0]); // chunk should be unaffected + // Should complete within a reasonable time + var completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(2))); + Assert.Same(waitTask, completed); + Assert.True(stream.IsCompleted); + } + + // ---- Completion signaling: ReportErrorAsync releases _completionSignal ---- + + /// + /// Test that ReportErrorAsync releases the completion signal. + /// Validates: Requirements 5.5 + /// + [Fact] + public async Task ReportErrorAsync_ReleasesCompletionSignal() + { + var (stream, _) = CreateWiredStream(); + + var waitTask = stream.WaitForCompletionAsync(); + Assert.False(waitTask.IsCompleted, "WaitForCompletionAsync should block before ReportErrorAsync"); + + await stream.ReportErrorAsync(new Exception("test error")); + + var completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(2))); + Assert.Same(waitTask, completed); + Assert.True(stream.HasError); } + // ---- Property 6: Size Limit Enforcement ---- + /// - /// Property 6: Size Limit Enforcement - Writing beyond 20 MiB throws InvalidOperationException. + /// Property 6: Size Limit Enforcement — single write exceeding limit throws. /// Validates: Requirements 3.6, 3.7 /// [Theory] - [InlineData(21 * 1024 * 1024)] // Single write exceeding limit + [InlineData(21 * 1024 * 1024)] public async Task SizeLimit_SingleWriteExceedingLimit_Throws(int writeSize) { - var stream = new ResponseStream(MaxResponseSize); + var (stream, _) = CreateWiredStream(); var data = new byte[writeSize]; await Assert.ThrowsAsync(() => stream.WriteAsync(data)); } /// - /// Property 6: Size Limit Enforcement - Multiple writes exceeding 20 MiB throws. + /// Property 6: Size Limit Enforcement — multiple writes exceeding limit throws. /// Validates: Requirements 3.6, 3.7 /// [Fact] public async Task SizeLimit_MultipleWritesExceedingLimit_Throws() { - var stream = new ResponseStream(MaxResponseSize); + var (stream, _) = CreateWiredStream(); await stream.WriteAsync(new byte[10 * 1024 * 1024]); await Assert.ThrowsAsync( @@ -128,7 +272,7 @@ await Assert.ThrowsAsync( [Fact] public async Task SizeLimit_ExactlyAtLimit_Succeeds() { - var stream = new ResponseStream(MaxResponseSize); + var (stream, _) = CreateWiredStream(); var data = new byte[20 * 1024 * 1024]; await stream.WriteAsync(data); @@ -136,14 +280,16 @@ public async Task SizeLimit_ExactlyAtLimit_Succeeds() Assert.Equal(MaxResponseSize, stream.BytesWritten); } + // ---- Property 19: Writes After Completion Rejected ---- + /// - /// Property 19: Writes After Completion Rejected - Writes after completion throw InvalidOperationException. + /// Property 19: Writes After Completion Rejected — writes after MarkCompleted throw. /// Validates: Requirements 8.8 /// [Fact] public async Task WriteAsync_AfterMarkCompleted_Throws() { - var stream = new ResponseStream(MaxResponseSize); + var (stream, _) = CreateWiredStream(); await stream.WriteAsync(new byte[] { 1 }); stream.MarkCompleted(); @@ -151,10 +297,14 @@ await Assert.ThrowsAsync( () => stream.WriteAsync(new byte[] { 2 })); } + /// + /// Property 19: Writes After Completion Rejected — writes after ReportErrorAsync throw. + /// Validates: Requirements 8.8 + /// [Fact] public async Task WriteAsync_AfterReportError_Throws() { - var stream = new ResponseStream(MaxResponseSize); + var (stream, _) = CreateWiredStream(); await stream.WriteAsync(new byte[] { 1 }); await stream.ReportErrorAsync(new Exception("test")); @@ -162,7 +312,7 @@ await Assert.ThrowsAsync( () => stream.WriteAsync(new byte[] { 2 })); } - // --- Error handling tests (2.6) --- + // ---- Error handling tests ---- [Fact] public async Task ReportErrorAsync_SetsErrorState() @@ -205,5 +355,47 @@ public void MarkCompleted_SetsCompletionState() Assert.True(stream.IsCompleted); } + + // ---- Argument validation ---- + + [Fact] + public async Task WriteAsync_NullBuffer_ThrowsArgumentNull() + { + var (stream, _) = CreateWiredStream(); + + await Assert.ThrowsAsync(() => stream.WriteAsync((byte[])null)); + } + + [Fact] + public async Task WriteAsync_NullBufferWithOffset_ThrowsArgumentNull() + { + var (stream, _) = CreateWiredStream(); + + await Assert.ThrowsAsync(() => stream.WriteAsync(null, 0, 0)); + } + + [Fact] + public async Task ReportErrorAsync_NullException_ThrowsArgumentNull() + { + var stream = new ResponseStream(MaxResponseSize); + + await Assert.ThrowsAsync(() => stream.ReportErrorAsync(null)); + } + + // ---- Dispose signals completion ---- + + [Fact] + public async Task Dispose_ReleasesCompletionSignalIfNotAlreadyReleased() + { + var stream = new ResponseStream(MaxResponseSize); + + var waitTask = stream.WaitForCompletionAsync(); + Assert.False(waitTask.IsCompleted); + + stream.Dispose(); + + var completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(2))); + Assert.Same(waitTask, completed); + } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs index 0682a816e..53b1e88b7 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs @@ -16,6 +16,7 @@ using System; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -25,149 +26,182 @@ public class StreamingHttpContentTests { private const long MaxResponseSize = 20 * 1024 * 1024; - private async Task SerializeContentAsync(StreamingHttpContent content) + /// + /// Helper: runs SerializeToStreamAsync concurrently with handler actions. + /// The handlerAction receives the ResponseStream and should write data then signal completion. + /// Returns the bytes written to the HTTP output stream. + /// + private async Task SerializeWithConcurrentHandler( + ResponseStream responseStream, + Func handlerAction) { - using var ms = new MemoryStream(); - await content.CopyToAsync(ms); - return ms.ToArray(); + var content = new StreamingHttpContent(responseStream); + var outputStream = new MemoryStream(); + + // Start serialization on a background task (it will call SetHttpOutputStream and wait) + var serializeTask = Task.Run(() => content.CopyToAsync(outputStream)); + + // Give SerializeToStreamAsync a moment to start and call SetHttpOutputStream + await Task.Delay(50); + + // Run the handler action (writes data, signals completion) + await handlerAction(responseStream); + + // Wait for serialization to complete + await serializeTask; + + return outputStream.ToArray(); } - // --- Task 5.4: Chunked encoding format tests --- + // ---- SerializeToStreamAsync hands off HTTP stream ---- /// - /// Property 9: Chunked Encoding Format - chunks are formatted as size(hex) + CRLF + data + CRLF. - /// Validates: Requirements 4.3, 10.1, 10.2 + /// Test that SerializeToStreamAsync calls SetHttpOutputStream on the ResponseStream, + /// enabling writes to flow through. + /// Validates: Requirements 4.3, 10.1 /// - [Theory] - [InlineData(1)] - [InlineData(10)] - [InlineData(255)] - [InlineData(4096)] - public async Task ChunkedEncoding_SingleChunk_CorrectFormat(int chunkSize) + [Fact] + public async Task SerializeToStreamAsync_HandsOffHttpStream_WritesFlowThrough() { - var stream = new ResponseStream(MaxResponseSize); - var data = new byte[chunkSize]; - for (int i = 0; i < data.Length; i++) data[i] = (byte)(i % 256); - await stream.WriteAsync(data); - - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - var outputStr = Encoding.ASCII.GetString(output); + var rs = new ResponseStream(MaxResponseSize); - var expectedSizeHex = chunkSize.ToString("X"); - Assert.StartsWith(expectedSizeHex + "\r\n", outputStr); - - // Verify chunk data follows the size line - var dataStart = expectedSizeHex.Length + 2; // size + CRLF - for (int i = 0; i < chunkSize; i++) + var output = await SerializeWithConcurrentHandler(rs, async stream => { - Assert.Equal(data[i], output[dataStart + i]); - } + await stream.WriteAsync(new byte[] { 0xAA, 0xBB }); + stream.MarkCompleted(); + }); - // Verify CRLF after data - Assert.Equal((byte)'\r', output[dataStart + chunkSize]); - Assert.Equal((byte)'\n', output[dataStart + chunkSize + 1]); + var outputStr = Encoding.ASCII.GetString(output); + // Should contain the chunk data written by the handler + Assert.Contains("2\r\n", outputStr); + Assert.True(output.Length > 0); } /// - /// Property 9: Chunked Encoding Format - multiple chunks each formatted correctly. - /// Validates: Requirements 4.3, 10.1 + /// Test that SerializeToStreamAsync blocks until MarkCompleted is called. + /// Validates: Requirements 4.3 /// [Fact] - public async Task ChunkedEncoding_MultipleChunks_EachFormattedCorrectly() + public async Task SerializeToStreamAsync_BlocksUntilMarkCompleted() { - var stream = new ResponseStream(MaxResponseSize); - await stream.WriteAsync(new byte[] { 0xAA, 0xBB }); - await stream.WriteAsync(new byte[] { 0xCC }); + var rs = new ResponseStream(MaxResponseSize); + var content = new StreamingHttpContent(rs); + var outputStream = new MemoryStream(); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - var outputStr = Encoding.ASCII.GetString(output); + var serializeTask = Task.Run(() => content.CopyToAsync(outputStream)); + await Task.Delay(50); + + // Serialization should still be running (waiting for completion) + Assert.False(serializeTask.IsCompleted, "SerializeToStreamAsync should block until completion is signaled"); + + // Now signal completion + rs.MarkCompleted(); + await serializeTask; - // First chunk: "2\r\n" + 2 bytes + "\r\n" - Assert.StartsWith("2\r\n", outputStr); - Assert.Equal(0xAA, output[3]); - Assert.Equal(0xBB, output[4]); - Assert.Equal((byte)'\r', output[5]); - Assert.Equal((byte)'\n', output[6]); - - // Second chunk: "1\r\n" + 1 byte + "\r\n" - Assert.Equal((byte)'1', output[7]); - Assert.Equal((byte)'\r', output[8]); - Assert.Equal((byte)'\n', output[9]); - Assert.Equal(0xCC, output[10]); - Assert.Equal((byte)'\r', output[11]); - Assert.Equal((byte)'\n', output[12]); + Assert.True(serializeTask.IsCompleted); } /// - /// Property 20: Final Chunk Termination - final chunk "0\r\n" is written. - /// Validates: Requirements 10.2, 10.5 + /// Test that SerializeToStreamAsync blocks until ReportErrorAsync is called. + /// Validates: Requirements 4.3, 5.1 /// [Fact] - public async Task FinalChunk_IsWritten() + public async Task SerializeToStreamAsync_BlocksUntilReportErrorAsync() { - var stream = new ResponseStream(MaxResponseSize); - await stream.WriteAsync(new byte[] { 1 }); + var rs = new ResponseStream(MaxResponseSize); + var content = new StreamingHttpContent(rs); + var outputStream = new MemoryStream(); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - var outputStr = Encoding.ASCII.GetString(output); + var serializeTask = Task.Run(() => content.CopyToAsync(outputStream)); + await Task.Delay(50); - // Output should end with final chunk "0\r\n" - Assert.EndsWith("0\r\n", outputStr); + Assert.False(serializeTask.IsCompleted, "SerializeToStreamAsync should block until error is reported"); + + await rs.ReportErrorAsync(new Exception("test error")); + await serializeTask; + + Assert.True(serializeTask.IsCompleted); } + // ---- Property 20: Final Chunk Termination ---- + + /// + /// Property 20: Final Chunk Termination — final chunk "0\r\n" is written after completion. + /// Validates: Requirements 4.3, 10.2, 10.3 + /// [Fact] - public async Task FinalChunk_EmptyStream_OnlyFinalChunk() + public async Task FinalChunk_WrittenAfterCompletion() { - var stream = new ResponseStream(MaxResponseSize); + var rs = new ResponseStream(MaxResponseSize); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); + var output = await SerializeWithConcurrentHandler(rs, async stream => + { + await stream.WriteAsync(new byte[] { 1 }); + stream.MarkCompleted(); + }); - Assert.Equal(Encoding.ASCII.GetBytes("0\r\n"), output); + var outputStr = Encoding.ASCII.GetString(output); + Assert.Contains("0\r\n", outputStr); + + // Final chunk should appear after the data chunk + var dataChunkEnd = outputStr.IndexOf("1\r\n") + 3 + 1 + 2; // "1\r\n" + 1 byte data + "\r\n" + var finalChunkIndex = outputStr.IndexOf("0\r\n", dataChunkEnd); + Assert.True(finalChunkIndex >= 0, "Final chunk 0\\r\\n should appear after data chunks"); } /// - /// Property 22: CRLF Line Terminators - all line terminators are CRLF, not just LF. - /// Validates: Requirements 10.5 + /// Property 20: Final Chunk Termination — empty stream still gets final chunk. + /// Validates: Requirements 10.2 /// [Fact] - public async Task CrlfTerminators_NoBareLineFeed() + public async Task FinalChunk_EmptyStream_StillWritten() { - var stream = new ResponseStream(MaxResponseSize); - await stream.WriteAsync(new byte[] { 65, 66, 67 }); // "ABC" + var rs = new ResponseStream(MaxResponseSize); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - - // Check every \n is preceded by \r - for (int i = 0; i < output.Length; i++) + var output = await SerializeWithConcurrentHandler(rs, stream => { - if (output[i] == (byte)'\n') - { - Assert.True(i > 0 && output[i - 1] == (byte)'\r', - $"Found bare LF at position {i} without preceding CR"); - } - } + stream.MarkCompleted(); + return Task.CompletedTask; + }); + + var outputStr = Encoding.ASCII.GetString(output); + Assert.StartsWith("0\r\n", outputStr); } + // ---- Property 21: Trailer Ordering ---- + + /// + /// Property 21: Trailer Ordering — trailers appear after final chunk. + /// Validates: Requirements 10.3 + /// [Fact] - public void TryComputeLength_ReturnsFalse() + public async Task ErrorTrailers_AppearAfterFinalChunk() { - var stream = new ResponseStream(MaxResponseSize); - var content = new StreamingHttpContent(stream); + var rs = new ResponseStream(MaxResponseSize); - var result = content.Headers.ContentLength; + var output = await SerializeWithConcurrentHandler(rs, async stream => + { + await stream.WriteAsync(new byte[] { 1 }); + await stream.ReportErrorAsync(new Exception("fail")); + }); - Assert.Null(result); + var outputStr = Encoding.UTF8.GetString(output); + + // Find the final chunk "0\r\n" that appears after data chunks + var dataEnd = outputStr.IndexOf("1\r\n") + 3 + 1 + 2; + var finalChunkIndex = outputStr.IndexOf("0\r\n", dataEnd); + var errorTypeIndex = outputStr.IndexOf("Lambda-Runtime-Function-Error-Type:"); + var errorBodyIndex = outputStr.IndexOf("Lambda-Runtime-Function-Error-Body:"); + + Assert.True(finalChunkIndex >= 0, "Final chunk not found"); + Assert.True(errorTypeIndex > finalChunkIndex, "Error type trailer should appear after final chunk"); + Assert.True(errorBodyIndex > finalChunkIndex, "Error body trailer should appear after final chunk"); } - // --- Task 5.6: Error trailer tests --- + // ---- Property 11: Midstream Error Type Trailer ---- /// - /// Property 11: Midstream Error Type Trailer - error type trailer is included for various exception types. + /// Property 11: Midstream Error Type Trailer — error type trailer is included for various exception types. /// Validates: Requirements 5.1, 5.2 /// [Theory] @@ -176,89 +210,138 @@ public void TryComputeLength_ReturnsFalse() [InlineData(typeof(NullReferenceException))] public async Task ErrorTrailer_IncludesErrorType(Type exceptionType) { - var stream = new ResponseStream(MaxResponseSize); - await stream.WriteAsync(new byte[] { 1 }); - var exception = (Exception)Activator.CreateInstance(exceptionType, "test error"); - await stream.ReportErrorAsync(exception); + var rs = new ResponseStream(MaxResponseSize); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - var outputStr = Encoding.UTF8.GetString(output); + var output = await SerializeWithConcurrentHandler(rs, async stream => + { + await stream.WriteAsync(new byte[] { 1 }); + var exception = (Exception)Activator.CreateInstance(exceptionType, "test error"); + await stream.ReportErrorAsync(exception); + }); + var outputStr = Encoding.UTF8.GetString(output); Assert.Contains($"Lambda-Runtime-Function-Error-Type: {exceptionType.Name}", outputStr); } + // ---- Property 12: Midstream Error Body Trailer ---- + /// - /// Property 12: Midstream Error Body Trailer - error body trailer includes JSON exception details. + /// Property 12: Midstream Error Body Trailer — error body trailer includes JSON exception details. /// Validates: Requirements 5.3 /// [Fact] public async Task ErrorTrailer_IncludesJsonErrorBody() { - var stream = new ResponseStream(MaxResponseSize); - await stream.WriteAsync(new byte[] { 1 }); - await stream.ReportErrorAsync(new InvalidOperationException("something went wrong")); + var rs = new ResponseStream(MaxResponseSize); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - var outputStr = Encoding.UTF8.GetString(output); + var output = await SerializeWithConcurrentHandler(rs, async stream => + { + await stream.WriteAsync(new byte[] { 1 }); + await stream.ReportErrorAsync(new InvalidOperationException("something went wrong")); + }); + var outputStr = Encoding.UTF8.GetString(output); Assert.Contains("Lambda-Runtime-Function-Error-Body:", outputStr); Assert.Contains("something went wrong", outputStr); Assert.Contains("InvalidOperationException", outputStr); } + // ---- Final CRLF termination ---- + /// - /// Property 21: Trailer Ordering - trailers appear after final chunk. - /// Validates: Requirements 10.3 + /// Test that the chunked message ends with CRLF after successful completion (no trailers). + /// Validates: Requirements 10.2, 10.5 /// [Fact] - public async Task ErrorTrailers_AppearAfterFinalChunk() + public async Task SuccessfulCompletion_EndsWithCrlf() { - var stream = new ResponseStream(MaxResponseSize); - await stream.WriteAsync(new byte[] { 1 }); - await stream.ReportErrorAsync(new Exception("fail")); + var rs = new ResponseStream(MaxResponseSize); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - var outputStr = Encoding.UTF8.GetString(output); + var output = await SerializeWithConcurrentHandler(rs, async stream => + { + await stream.WriteAsync(new byte[] { 1 }); + stream.MarkCompleted(); + }); - var finalChunkIndex = outputStr.IndexOf("0\r\n"); - var errorTypeIndex = outputStr.IndexOf("Lambda-Runtime-Function-Error-Type:"); - var errorBodyIndex = outputStr.IndexOf("Lambda-Runtime-Function-Error-Body:"); + var outputStr = Encoding.ASCII.GetString(output); + // Should end with "0\r\n" (final chunk) + "\r\n" (end of message) + Assert.EndsWith("0\r\n\r\n", outputStr); + } - Assert.True(finalChunkIndex >= 0, "Final chunk not found"); - Assert.True(errorTypeIndex > finalChunkIndex, "Error type trailer should appear after final chunk"); - Assert.True(errorBodyIndex > finalChunkIndex, "Error body trailer should appear after final chunk"); + /// + /// Test that the chunked message ends with CRLF after error trailers. + /// Validates: Requirements 10.3, 10.5 + /// + [Fact] + public async Task ErrorCompletion_EndsWithCrlf() + { + var rs = new ResponseStream(MaxResponseSize); + + var output = await SerializeWithConcurrentHandler(rs, async stream => + { + await stream.WriteAsync(new byte[] { 1 }); + await stream.ReportErrorAsync(new Exception("fail")); + }); + + var outputStr = Encoding.UTF8.GetString(output); + Assert.EndsWith("\r\n", outputStr); } + // ---- No error, no trailers ---- + [Fact] public async Task NoError_NoTrailersWritten() { - var stream = new ResponseStream(MaxResponseSize); - await stream.WriteAsync(new byte[] { 1 }); + var rs = new ResponseStream(MaxResponseSize); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - var outputStr = Encoding.UTF8.GetString(output); + var output = await SerializeWithConcurrentHandler(rs, async stream => + { + await stream.WriteAsync(new byte[] { 1 }); + stream.MarkCompleted(); + }); + var outputStr = Encoding.UTF8.GetString(output); Assert.DoesNotContain("Lambda-Runtime-Function-Error-Type:", outputStr); Assert.DoesNotContain("Lambda-Runtime-Function-Error-Body:", outputStr); } + // ---- TryComputeLength ---- + [Fact] - public async Task ErrorTrailers_EndWithCrlf() + public void TryComputeLength_ReturnsFalse() { var stream = new ResponseStream(MaxResponseSize); - await stream.WriteAsync(new byte[] { 1 }); - await stream.ReportErrorAsync(new Exception("fail")); - var content = new StreamingHttpContent(stream); - var output = await SerializeContentAsync(content); - var outputStr = Encoding.UTF8.GetString(output); - // Should end with final CRLF after trailers - Assert.EndsWith("\r\n", outputStr); + var result = content.Headers.ContentLength; + Assert.Null(result); + } + + // ---- CRLF correctness ---- + + /// + /// Property 22: CRLF Line Terminators — all line terminators are CRLF, not just LF. + /// Validates: Requirements 10.5 + /// + [Fact] + public async Task CrlfTerminators_NoBareLineFeed() + { + var rs = new ResponseStream(MaxResponseSize); + + var output = await SerializeWithConcurrentHandler(rs, async stream => + { + await stream.WriteAsync(new byte[] { 65, 66, 67 }); // "ABC" + stream.MarkCompleted(); + }); + + for (int i = 0; i < output.Length; i++) + { + if (output[i] == (byte)'\n') + { + Assert.True(i > 0 && output[i - 1] == (byte)'\r', + $"Found bare LF at position {i} without preceding CR"); + } + } } } } From 603612d1635ebc5fcf6091c30afb005605f7abd4 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 16 Feb 2026 18:14:44 -0800 Subject: [PATCH 07/13] Task 5 --- .../Client/ResponseStreamFactory.cs | 25 +++- .../Client/RuntimeApiClient.cs | 41 ++++++ .../ResponseStreamFactoryTests.cs | 135 +++++++++++++++--- .../NoOpInternalRuntimeApiClient.cs | 60 ++++++++ 4 files changed, 238 insertions(+), 23 deletions(-) create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/NoOpInternalRuntimeApiClient.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs index 9b60eacfd..613980fb1 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs @@ -15,6 +15,7 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace Amazon.Lambda.RuntimeSupport { @@ -57,19 +58,29 @@ public static IResponseStream CreateStream() context.Stream = stream; context.StreamCreated = true; + // Start the HTTP POST to the Runtime API. + // This runs concurrently — SerializeToStreamAsync will block + // until the handler finishes writing or reports an error. + context.SendTask = context.RuntimeApiClient.StartStreamingResponseAsync( + context.AwsRequestId, stream, context.CancellationToken); + return stream; } // Internal methods for LambdaBootstrap to manage state - internal static void InitializeInvocation(string awsRequestId, long maxResponseSize, bool isMultiConcurrency) + internal static void InitializeInvocation( + string awsRequestId, long maxResponseSize, bool isMultiConcurrency, + RuntimeApiClient runtimeApiClient, CancellationToken cancellationToken) { var context = new ResponseStreamContext { AwsRequestId = awsRequestId, MaxResponseSize = maxResponseSize, StreamCreated = false, - Stream = null + Stream = null, + RuntimeApiClient = runtimeApiClient, + CancellationToken = cancellationToken }; if (isMultiConcurrency) @@ -88,6 +99,16 @@ internal static ResponseStream GetStreamIfCreated(bool isMultiConcurrency) return context?.Stream; } + /// + /// Returns the Task for the in-flight HTTP send, or null if streaming wasn't started. + /// LambdaBootstrap awaits this after the handler returns to ensure the HTTP request completes. + /// + internal static Task GetSendTask(bool isMultiConcurrency) + { + var context = isMultiConcurrency ? _asyncLocalContext.Value : _onDemandContext; + return context?.SendTask; + } + internal static void CleanupInvocation(bool isMultiConcurrency) { if (isMultiConcurrency) diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs index daa9fff24..13c4e4eac 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs @@ -177,6 +177,47 @@ public Task ReportRestoreErrorAsync(Exception exception, String errorType = null #endif + /// + /// Start sending a streaming response to the Runtime API. + /// This initiates the HTTP POST with streaming headers. The actual data + /// is written by the handler via ResponseStream.WriteAsync, which flows + /// through StreamingHttpContent to the HTTP connection. + /// This Task completes when the stream is finalized (MarkCompleted or error). + /// + /// The ID of the function request being responded to. + /// The ResponseStream that will provide the streaming data. + /// The optional cancellation token to use. + /// A Task representing the in-flight HTTP POST. + internal virtual async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + if (awsRequestId == null) throw new ArgumentNullException(nameof(awsRequestId)); + if (responseStream == null) throw new ArgumentNullException(nameof(responseStream)); + + var url = $"http://{LambdaEnvironment.RuntimeServerHostAndPort}/2018-06-01/runtime/invocation/{awsRequestId}/response"; + + using (var request = new HttpRequestMessage(HttpMethod.Post, url)) + { + request.Headers.Add(StreamingConstants.ResponseModeHeader, StreamingConstants.StreamingResponseMode); + request.Headers.TransferEncodingChunked = true; + + // Declare trailers upfront — we always declare them since we don't know + // at request start time whether an error will occur mid-stream. + request.Headers.Add("Trailer", + $"{StreamingConstants.ErrorTypeTrailer}, {StreamingConstants.ErrorBodyTrailer}"); + + request.Content = new StreamingHttpContent(responseStream); + + // SendAsync calls SerializeToStreamAsync, which blocks until the handler + // finishes writing. This is why this method runs concurrently with the handler. + var response = await _httpClient.SendAsync( + request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + response.EnsureSuccessStatusCode(); + } + + responseStream.MarkCompleted(); + } + /// /// Send a response to a function invocation to the Runtime API as an asynchronous operation. /// diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs index a4b0558af..11973ae5f 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs @@ -14,6 +14,7 @@ */ using System; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -30,7 +31,40 @@ public void Dispose() ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); } - // --- Task 3.3: CreateStream tests --- + /// + /// A minimal RuntimeApiClient subclass for testing that overrides StartStreamingResponseAsync + /// to avoid real HTTP calls while tracking invocations. + /// + private class MockStreamingRuntimeApiClient : RuntimeApiClient + { + public bool StartStreamingCalled { get; private set; } + public string LastAwsRequestId { get; private set; } + public ResponseStream LastResponseStream { get; private set; } + public TaskCompletionSource SendTaskCompletion { get; } = new TaskCompletionSource(); + + public MockStreamingRuntimeApiClient() + : base(new TestEnvironmentVariables(), new TestHelpers.NoOpInternalRuntimeApiClient()) + { + } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + StartStreamingCalled = true; + LastAwsRequestId = awsRequestId; + LastResponseStream = responseStream; + await SendTaskCompletion.Task; + } + } + + private void InitializeWithMock(string requestId, bool isMultiConcurrency, MockStreamingRuntimeApiClient mockClient) + { + ResponseStreamFactory.InitializeInvocation( + requestId, MaxResponseSize, isMultiConcurrency, + mockClient, CancellationToken.None); + } + + // --- Property 1: CreateStream Returns Valid Stream --- /// /// Property 1: CreateStream Returns Valid Stream - on-demand mode. @@ -39,7 +73,8 @@ public void Dispose() [Fact] public void CreateStream_OnDemandMode_ReturnsValidStream() { - ResponseStreamFactory.InitializeInvocation("req-1", MaxResponseSize, isMultiConcurrency: false); + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-1", isMultiConcurrency: false, mock); var stream = ResponseStreamFactory.CreateStream(); @@ -54,7 +89,8 @@ public void CreateStream_OnDemandMode_ReturnsValidStream() [Fact] public void CreateStream_MultiConcurrencyMode_ReturnsValidStream() { - ResponseStreamFactory.InitializeInvocation("req-2", MaxResponseSize, isMultiConcurrency: true); + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-2", isMultiConcurrency: true, mock); var stream = ResponseStreamFactory.CreateStream(); @@ -62,6 +98,8 @@ public void CreateStream_MultiConcurrencyMode_ReturnsValidStream() Assert.IsAssignableFrom(stream); } + // --- Property 4: Single Stream Per Invocation --- + /// /// Property 4: Single Stream Per Invocation - calling CreateStream twice throws. /// Validates: Requirements 2.5, 2.6 @@ -69,7 +107,8 @@ public void CreateStream_MultiConcurrencyMode_ReturnsValidStream() [Fact] public void CreateStream_CalledTwice_ThrowsInvalidOperationException() { - ResponseStreamFactory.InitializeInvocation("req-3", MaxResponseSize, isMultiConcurrency: false); + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-3", isMultiConcurrency: false, mock); ResponseStreamFactory.CreateStream(); Assert.Throws(() => ResponseStreamFactory.CreateStream()); @@ -82,17 +121,69 @@ public void CreateStream_OutsideInvocationContext_ThrowsInvalidOperationExceptio Assert.Throws(() => ResponseStreamFactory.CreateStream()); } - // --- Task 3.5: Internal methods tests --- + // --- CreateStream starts HTTP POST --- + + /// + /// Validates that CreateStream calls StartStreamingResponseAsync on the RuntimeApiClient. + /// Validates: Requirements 1.3, 1.4, 2.2, 2.3, 2.4 + /// + [Fact] + public void CreateStream_CallsStartStreamingResponseAsync() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-start", isMultiConcurrency: false, mock); + + ResponseStreamFactory.CreateStream(); + + Assert.True(mock.StartStreamingCalled); + Assert.Equal("req-start", mock.LastAwsRequestId); + Assert.NotNull(mock.LastResponseStream); + } + + // --- GetSendTask --- + + /// + /// Validates that GetSendTask returns the task from the HTTP POST. + /// Validates: Requirements 5.1, 7.3 + /// + [Fact] + public void GetSendTask_AfterCreateStream_ReturnsNonNullTask() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-send", isMultiConcurrency: false, mock); + + ResponseStreamFactory.CreateStream(); + + var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency: false); + Assert.NotNull(sendTask); + } + + [Fact] + public void GetSendTask_BeforeCreateStream_ReturnsNull() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-nosend", isMultiConcurrency: false, mock); + + var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency: false); + Assert.Null(sendTask); + } + + [Fact] + public void GetSendTask_NoContext_ReturnsNull() + { + Assert.Null(ResponseStreamFactory.GetSendTask(isMultiConcurrency: false)); + } + + // --- Internal methods --- [Fact] public void InitializeInvocation_OnDemand_SetsUpContext() { - ResponseStreamFactory.InitializeInvocation("req-4", MaxResponseSize, isMultiConcurrency: false); + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-4", isMultiConcurrency: false, mock); - // GetStreamIfCreated should return null since CreateStream hasn't been called Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); - // But CreateStream should work (proving context was set up) var stream = ResponseStreamFactory.CreateStream(); Assert.NotNull(stream); } @@ -100,7 +191,8 @@ public void InitializeInvocation_OnDemand_SetsUpContext() [Fact] public void InitializeInvocation_MultiConcurrency_SetsUpContext() { - ResponseStreamFactory.InitializeInvocation("req-5", MaxResponseSize, isMultiConcurrency: true); + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-5", isMultiConcurrency: true, mock); Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true)); @@ -111,11 +203,11 @@ public void InitializeInvocation_MultiConcurrency_SetsUpContext() [Fact] public void GetStreamIfCreated_AfterCreateStream_ReturnsStream() { - ResponseStreamFactory.InitializeInvocation("req-6", MaxResponseSize, isMultiConcurrency: false); - var created = ResponseStreamFactory.CreateStream(); + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-6", isMultiConcurrency: false, mock); + ResponseStreamFactory.CreateStream(); var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false); - Assert.NotNull(retrieved); } @@ -128,7 +220,8 @@ public void GetStreamIfCreated_NoContext_ReturnsNull() [Fact] public void CleanupInvocation_ClearsState() { - ResponseStreamFactory.InitializeInvocation("req-7", MaxResponseSize, isMultiConcurrency: false); + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-7", isMultiConcurrency: false, mock); ResponseStreamFactory.CreateStream(); ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); @@ -137,6 +230,8 @@ public void CleanupInvocation_ClearsState() Assert.Throws(() => ResponseStreamFactory.CreateStream()); } + // --- Property 16: State Isolation Between Invocations --- + /// /// Property 16: State Isolation Between Invocations - state from one invocation doesn't leak to the next. /// Validates: Requirements 6.5, 8.9 @@ -144,17 +239,18 @@ public void CleanupInvocation_ClearsState() [Fact] public void StateIsolation_SequentialInvocations_NoLeakage() { + var mock = new MockStreamingRuntimeApiClient(); + // First invocation - streaming - ResponseStreamFactory.InitializeInvocation("req-8a", MaxResponseSize, isMultiConcurrency: false); + InitializeWithMock("req-8a", isMultiConcurrency: false, mock); var stream1 = ResponseStreamFactory.CreateStream(); Assert.NotNull(stream1); ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); // Second invocation - should start fresh - ResponseStreamFactory.InitializeInvocation("req-8b", MaxResponseSize, isMultiConcurrency: false); + InitializeWithMock("req-8b", isMultiConcurrency: false, mock); Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); - // Should be able to create a new stream var stream2 = ResponseStreamFactory.CreateStream(); Assert.NotNull(stream2); ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); @@ -167,17 +263,14 @@ public void StateIsolation_SequentialInvocations_NoLeakage() [Fact] public async Task StateIsolation_MultiConcurrency_UsesAsyncLocal() { - // Initialize in multi-concurrency mode on main thread - ResponseStreamFactory.InitializeInvocation("req-9", MaxResponseSize, isMultiConcurrency: true); + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-9", isMultiConcurrency: true, mock); var stream = ResponseStreamFactory.CreateStream(); Assert.NotNull(stream); - // A separate task should not see the main thread's context - // (AsyncLocal flows to child tasks, but a fresh Task.Run with new initialization should override) bool childSawNull = false; await Task.Run(() => { - // Clean up the flowed context first ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); childSawNull = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true) == null; }); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/NoOpInternalRuntimeApiClient.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/NoOpInternalRuntimeApiClient.cs new file mode 100644 index 000000000..9fa0434cd --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/NoOpInternalRuntimeApiClient.cs @@ -0,0 +1,60 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers +{ + /// + /// A no-op implementation of IInternalRuntimeApiClient for unit tests + /// that need to construct a RuntimeApiClient without real HTTP calls. + /// + internal class NoOpInternalRuntimeApiClient : IInternalRuntimeApiClient + { + private static readonly SwaggerResponse EmptyStatusResponse = + new SwaggerResponse(200, new Dictionary>(), new StatusResponse()); + + public Task> ErrorAsync( + string lambda_Runtime_Function_Error_Type, string errorJson, CancellationToken cancellationToken) + => Task.FromResult(EmptyStatusResponse); + + public Task> NextAsync(CancellationToken cancellationToken) + => Task.FromResult(new SwaggerResponse(200, new Dictionary>(), Stream.Null)); + + public Task> ResponseAsync(string awsRequestId, Stream outputStream) + => Task.FromResult(EmptyStatusResponse); + + public Task> ResponseAsync( + string awsRequestId, Stream outputStream, CancellationToken cancellationToken) + => Task.FromResult(EmptyStatusResponse); + + public Task> ErrorWithXRayCauseAsync( + string awsRequestId, string lambda_Runtime_Function_Error_Type, + string errorJson, string xrayCause, CancellationToken cancellationToken) + => Task.FromResult(EmptyStatusResponse); + +#if NET8_0_OR_GREATER + public Task> RestoreNextAsync(CancellationToken cancellationToken) + => Task.FromResult(new SwaggerResponse(200, new Dictionary>(), Stream.Null)); + + public Task> RestoreErrorAsync( + string lambda_Runtime_Function_Error_Type, string errorJson, CancellationToken cancellationToken) + => Task.FromResult(EmptyStatusResponse); +#endif + } +} From 63224bf43b63d1edee3b92576d70c965bbaa60e7 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 16 Feb 2026 23:44:57 -0800 Subject: [PATCH 08/13] Task 6 --- .../RuntimeApiClientTests.cs | 223 ++++ .../serverless.template | 659 +--------- .../serverless.template | 22 +- .../TestServerlessApp/serverless.template | 1149 +---------------- 4 files changed, 228 insertions(+), 1825 deletions(-) create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs new file mode 100644 index 000000000..75abec101 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs @@ -0,0 +1,223 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + /// + /// Tests for RuntimeApiClient streaming and buffered behavior. + /// Validates Properties 7, 8, 10, 13, 18. + /// + public class RuntimeApiClientTests + { + private const long MaxResponseSize = 20 * 1024 * 1024; + + /// + /// Mock HttpMessageHandler that captures the request for header inspection. + /// It completes the ResponseStream and returns immediately without reading + /// the content body, avoiding the SerializeToStreamAsync blocking issue. + /// + private class MockHttpMessageHandler : HttpMessageHandler + { + public HttpRequestMessage CapturedRequest { get; private set; } + private readonly ResponseStream _responseStream; + + public MockHttpMessageHandler(ResponseStream responseStream) + { + _responseStream = responseStream; + } + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + CapturedRequest = request; + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + } + + private static RuntimeApiClient CreateClientWithMockHandler( + ResponseStream stream, out MockHttpMessageHandler handler) + { + handler = new MockHttpMessageHandler(stream); + var httpClient = new HttpClient(handler); + var envVars = new TestEnvironmentVariables(); + envVars.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", "localhost:9001"); + return new RuntimeApiClient(envVars, httpClient); + } + + // --- Property 7: Streaming Response Mode Header --- + + /// + /// Property 7: Streaming Response Mode Header + /// For any streaming response, the HTTP request should include + /// "Lambda-Runtime-Function-Response-Mode: streaming". + /// **Validates: Requirements 4.1** + /// + [Fact] + public async Task StartStreamingResponseAsync_IncludesStreamingResponseModeHeader() + { + var stream = new ResponseStream(MaxResponseSize); + var client = CreateClientWithMockHandler(stream, out var handler); + + await client.StartStreamingResponseAsync("req-1", stream, CancellationToken.None); + + Assert.NotNull(handler.CapturedRequest); + Assert.True(handler.CapturedRequest.Headers.Contains(StreamingConstants.ResponseModeHeader)); + var values = handler.CapturedRequest.Headers.GetValues(StreamingConstants.ResponseModeHeader).ToList(); + Assert.Single(values); + Assert.Equal(StreamingConstants.StreamingResponseMode, values[0]); + } + + // --- Property 8: Chunked Transfer Encoding Header --- + + /// + /// Property 8: Chunked Transfer Encoding Header + /// For any streaming response, the HTTP request should include + /// "Transfer-Encoding: chunked". + /// **Validates: Requirements 4.2** + /// + [Fact] + public async Task StartStreamingResponseAsync_IncludesChunkedTransferEncodingHeader() + { + var stream = new ResponseStream(MaxResponseSize); + var client = CreateClientWithMockHandler(stream, out var handler); + + await client.StartStreamingResponseAsync("req-2", stream, CancellationToken.None); + + Assert.NotNull(handler.CapturedRequest); + Assert.True(handler.CapturedRequest.Headers.TransferEncodingChunked); + } + + // --- Property 13: Trailer Declaration Header --- + + /// + /// Property 13: Trailer Declaration Header + /// For any streaming response, the HTTP request should include a "Trailer" header + /// declaring the error trailer headers upfront (since we cannot know at request + /// start whether an error will occur). + /// **Validates: Requirements 5.4** + /// + [Fact] + public async Task StartStreamingResponseAsync_DeclaresTrailerHeaderUpfront() + { + var stream = new ResponseStream(MaxResponseSize); + var client = CreateClientWithMockHandler(stream, out var handler); + + await client.StartStreamingResponseAsync("req-3", stream, CancellationToken.None); + + Assert.NotNull(handler.CapturedRequest); + Assert.True(handler.CapturedRequest.Headers.Contains("Trailer")); + var trailerValue = string.Join(", ", handler.CapturedRequest.Headers.GetValues("Trailer")); + Assert.Contains(StreamingConstants.ErrorTypeTrailer, trailerValue); + Assert.Contains(StreamingConstants.ErrorBodyTrailer, trailerValue); + } + + // --- Property 18: Stream Finalization --- + + /// + /// Property 18: Stream Finalization + /// For any streaming response that completes successfully, the ResponseStream + /// should be marked as completed (IsCompleted = true) after the HTTP response succeeds. + /// **Validates: Requirements 8.3** + /// + [Fact] + public async Task StartStreamingResponseAsync_MarksStreamCompletedAfterSuccess() + { + var stream = new ResponseStream(MaxResponseSize); + var client = CreateClientWithMockHandler(stream, out _); + + await client.StartStreamingResponseAsync("req-4", stream, CancellationToken.None); + + Assert.True(stream.IsCompleted); + } + + // --- Property 10: Buffered Responses Exclude Streaming Headers --- + + /// + /// Mock HttpMessageHandler that captures the request for buffered response header inspection. + /// Returns an Accepted (202) response since that's what the InternalRuntimeApiClient expects. + /// + private class BufferedMockHttpMessageHandler : HttpMessageHandler + { + public HttpRequestMessage CapturedRequest { get; private set; } + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + CapturedRequest = request; + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Accepted)); + } + } + + /// + /// Property 10: Buffered Responses Exclude Streaming Headers + /// For any buffered response (where CreateStream was not called), the HTTP request + /// should not include "Lambda-Runtime-Function-Response-Mode" or + /// "Transfer-Encoding: chunked" or "Trailer" headers. + /// **Validates: Requirements 4.6** + /// + [Fact] + public async Task SendResponseAsync_BufferedResponse_ExcludesStreamingHeaders() + { + var bufferedHandler = new BufferedMockHttpMessageHandler(); + var httpClient = new HttpClient(bufferedHandler); + var envVars = new TestEnvironmentVariables(); + envVars.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", "localhost:9001"); + var client = new RuntimeApiClient(envVars, httpClient); + + var outputStream = new MemoryStream(new byte[] { 1, 2, 3 }); + await client.SendResponseAsync("req-buffered", outputStream, CancellationToken.None); + + Assert.NotNull(bufferedHandler.CapturedRequest); + // Buffered responses must not include streaming-specific headers + Assert.False(bufferedHandler.CapturedRequest.Headers.Contains(StreamingConstants.ResponseModeHeader), + "Buffered response should not include Lambda-Runtime-Function-Response-Mode header"); + Assert.NotEqual(true, bufferedHandler.CapturedRequest.Headers.TransferEncodingChunked); + Assert.False(bufferedHandler.CapturedRequest.Headers.Contains("Trailer"), + "Buffered response should not include Trailer header"); + } + + // --- Argument validation --- + + [Fact] + public async Task StartStreamingResponseAsync_NullRequestId_ThrowsArgumentNullException() + { + var stream = new ResponseStream(MaxResponseSize); + var client = CreateClientWithMockHandler(stream, out _); + + await Assert.ThrowsAsync( + () => client.StartStreamingResponseAsync(null, stream, CancellationToken.None)); + } + + [Fact] + public async Task StartStreamingResponseAsync_NullResponseStream_ThrowsArgumentNullException() + { + var stream = new ResponseStream(MaxResponseSize); + var client = CreateClientWithMockHandler(stream, out _); + + await Assert.ThrowsAsync( + () => client.StartStreamingResponseAsync("req-5", null, CancellationToken.None)); + } + } +} diff --git a/Libraries/test/TestExecutableServerlessApp/serverless.template b/Libraries/test/TestExecutableServerlessApp/serverless.template index ac43959b7..229385aba 100644 --- a/Libraries/test/TestExecutableServerlessApp/serverless.template +++ b/Libraries/test/TestExecutableServerlessApp/serverless.template @@ -1,7 +1,7 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", + "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.8.0.0).", "Parameters": { "ArchitectureTypeParameter": { "Type": "String", @@ -21,662 +21,7 @@ ] } }, - "Resources": { - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeader" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheaderasync/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "DynamicReturn" - } - } - } - }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "DynamicInput" - } - } - } - }, - "GreeterSayHello": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 1024, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "SayHello" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHello", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "GreeterSayHelloAsync": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 50, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "SayHelloAsync" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHelloAsync", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "HasIntrinsic" - } - } - } - }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/nullableheaderhttpapi", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppParameterlessMethodsNoParameterGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NoParameter" - } - } - } - }, - "TestServerlessAppParameterlessMethodWithResponseNoParameterWithResponseGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NoParameterWithResponse" - } - } - } - }, - "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "GetPerson" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/", - "Method": "GET" - } - } - } - } - }, - "ToUpper": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "ToUpper" - } - } - } - }, - "ToLower": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "ToLower" - } - } - } - }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "TaskReturn" - } - } - } - }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "VoidReturn" - } - } - } - } - }, + "Resources": {}, "Outputs": { "RestApiURL": { "Description": "Rest API endpoint URL for Prod environment", diff --git a/Libraries/test/TestServerlessApp.NET8/serverless.template b/Libraries/test/TestServerlessApp.NET8/serverless.template index c42ff4a47..67ec5dfa4 100644 --- a/Libraries/test/TestServerlessApp.NET8/serverless.template +++ b/Libraries/test/TestServerlessApp.NET8/serverless.template @@ -1,24 +1,6 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Resources": { - "TestServerlessAppNET8FunctionsToUpperGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "dotnet8", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestServerlessApp.NET8::TestServerlessApp.NET8.Functions_ToUpper_Generated::ToUpper" - } - } - } + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.8.0.0).", + "Resources": {} } \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 0e3befbe1..e6c1b8bea 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -1,7 +1,7 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", + "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.8.0.0).", "Parameters": { "ArchitectureTypeParameter": { "Type": "String", @@ -28,1153 +28,6 @@ "Resources": { "TestQueue": { "Type": "AWS::SQS::Queue" - }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" - ] - } - } - }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" - ] - } - } - }, - "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/{text}", - "Method": "GET" - } - } - } - } - }, - "HttpApiAuthorizerTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer", - "Method": "GET" - } - } - } - } - }, - "SimpleCalculatorAdd": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/SimpleCalculator/Add", - "Method": "GET" - } - } - } - } - }, - "SimpleCalculatorSubtract": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/SimpleCalculator/Subtract", - "Method": "GET" - } - } - } - } - }, - "SimpleCalculatorMultiply": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/SimpleCalculator/Multiply/{x}/{y}", - "Method": "GET" - } - } - } - } - }, - "SimpleCalculatorDivideAsync": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", - "Method": "GET" - } - } - } - } - }, - "PI": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" - ] - } - } - }, - "Random": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" - ] - } - } - }, - "Randoms": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" - ] - } - } - }, - "SQSMessageHandler": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "TestQueueEvent" - ], - "SyncedEventProperties": { - "TestQueueEvent": [ - "Queue.Fn::GetAtt", - "BatchSize", - "FilterCriteria.Filters", - "FunctionResponseTypes", - "MaximumBatchingWindowInSeconds", - "ScalingConfig.MaximumConcurrency" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaSQSQueueExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" - ] - }, - "Events": { - "TestQueueEvent": { - "Type": "SQS", - "Properties": { - "BatchSize": 50, - "FilterCriteria": { - "Filters": [ - { - "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" - } - ] - }, - "FunctionResponseTypes": [ - "ReportBatchItemFailures" - ], - "MaximumBatchingWindowInSeconds": 5, - "ScalingConfig": { - "MaximumConcurrency": 5 - }, - "Queue": { - "Fn::GetAtt": [ - "TestQueue", - "Arn" - ] - } - } - } - } - } - }, - "HttpApiV1AuthorizerTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer-v1", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/nullableheaderhttpapi", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/authorizerihttpresults", - "Method": "GET" - } - } - } - } - }, - "GreeterSayHello": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 1024, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHello", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "GreeterSayHelloAsync": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 50, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHelloAsync", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheaderasync/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" - ] - } - } - }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" - ] - } - } - }, - "HttpApiNonString": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer-non-string", - "Method": "GET" - } - } - } - } - }, - "AuthNameFallbackTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer-fallback", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" - ] - } - } - }, - "RestAuthorizerTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/rest/authorizer", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" - ] - } - } - }, - "ToUpper": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" - ] - } - } - }, - "TestServerlessAppComplexCalculatorAddGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootPost" - ], - "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" - ] - }, - "Events": { - "RootPost": { - "Type": "HttpApi", - "Properties": { - "Path": "/ComplexCalculator/Add", - "Method": "POST" - } - } - } - } - }, - "TestServerlessAppComplexCalculatorSubtractGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootPost" - ], - "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" - ] - }, - "Events": { - "RootPost": { - "Type": "HttpApi", - "Properties": { - "Path": "/ComplexCalculator/Subtract", - "Method": "POST" - } - } - } - } } }, "Outputs": { From 14c993b08f6c6d44420f085d1d0a5c29ee9d1030 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Tue, 17 Feb 2026 18:46:16 -0800 Subject: [PATCH 09/13] Task 8 --- .../Bootstrap/LambdaBootstrap.cs | 45 ++++- .../LambdaBootstrapTests.cs | 156 ++++++++++++++++++ .../TestStreamingRuntimeApiClient.cs | 131 +++++++++++++++ 3 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs index 0e00f3e7f..68b67c339 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -349,6 +349,7 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul _logger.LogInformation("Starting InvokeOnceAsync"); var invocation = await Client.GetNextInvocationAsync(cancellationToken); + var isMultiConcurrency = Utils.IsUsingMultiConcurrency(_environmentVariables); Func processingFunc = async () => { @@ -358,6 +359,18 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul SetInvocationTraceId(impl.RuntimeApiHeaders.TraceId); } + // Initialize ResponseStreamFactory — includes RuntimeApiClient reference + var runtimeApiClient = Client as RuntimeApiClient; + if (runtimeApiClient != null) + { + ResponseStreamFactory.InitializeInvocation( + invocation.LambdaContext.AwsRequestId, + StreamingConstants.MaxResponseSize, + isMultiConcurrency, + runtimeApiClient, + cancellationToken); + } + try { InvocationResponse response = null; @@ -372,15 +385,39 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul catch (Exception exception) { WriteUnhandledExceptionToLog(exception); - await Client.ReportInvocationErrorAsync(invocation.LambdaContext.AwsRequestId, exception, cancellationToken); + + var streamIfCreated = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency); + if (streamIfCreated != null && streamIfCreated.BytesWritten > 0) + { + // Midstream error — report via trailers on the already-open HTTP connection + await streamIfCreated.ReportErrorAsync(exception); + } + else + { + // Error before streaming started — use standard error reporting + await Client.ReportInvocationErrorAsync(invocation.LambdaContext.AwsRequestId, exception, cancellationToken); + } } finally { _logger.LogInformation("Finished invoking handler"); } - if (invokeSucceeded) + // If streaming was started, await the HTTP send task to ensure it completes + var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency); + if (sendTask != null) { + var stream = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency); + if (stream != null && !stream.IsCompleted && !stream.HasError) + { + // Handler returned successfully — signal stream completion + stream.MarkCompleted(); + } + await sendTask; // Wait for HTTP request to finish + } + else if (invokeSucceeded) + { + // No streaming — send buffered response _logger.LogInformation("Starting sending response"); try { @@ -415,6 +452,10 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul } finally { + if (runtimeApiClient != null) + { + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency); + } invocation.Dispose(); } }; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs index e1636ff16..07c2379a0 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs @@ -14,9 +14,11 @@ */ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -283,5 +285,159 @@ public void IsCallPreJitTest() environmentVariables.SetEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_LAMBDA_INITIALIZATION_TYPE, AWS_LAMBDA_INITIALIZATION_TYPE_PC); Assert.True(UserCodeInit.IsCallPreJit(environmentVariables)); } + + // --- Streaming Integration Tests --- + + private TestStreamingRuntimeApiClient CreateStreamingClient() + { + var envVars = new TestEnvironmentVariables(); + var headers = new Dictionary> + { + { RuntimeApiHeaders.HeaderAwsRequestId, new List { "streaming-request-id" } }, + { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List { "invoked_function_arn" } }, + { RuntimeApiHeaders.HeaderAwsTenantId, new List { "tenant_id" } } + }; + return new TestStreamingRuntimeApiClient(envVars, headers); + } + + /// + /// Property 2: CreateStream Enables Streaming Mode + /// When a handler calls ResponseStreamFactory.CreateStream(), the response is transmitted + /// using streaming mode. LambdaBootstrap awaits the send task. + /// **Validates: Requirements 1.4, 6.1, 6.2, 6.3, 6.4** + /// + [Fact] + public async Task StreamingMode_HandlerCallsCreateStream_SendTaskAwaited() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + await stream.WriteAsync(Encoding.UTF8.GetBytes("hello")); + return new InvocationResponse(Stream.Null, false); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null)) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + Assert.True(streamingClient.StartStreamingResponseAsyncCalled); + Assert.False(streamingClient.SendResponseAsyncCalled); + } + + /// + /// Property 3: Default Mode Is Buffered + /// When a handler does not call ResponseStreamFactory.CreateStream(), the response + /// is transmitted using buffered mode via SendResponseAsync. + /// **Validates: Requirements 1.5, 7.2** + /// + [Fact] + public async Task BufferedMode_HandlerDoesNotCallCreateStream_UsesSendResponse() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var outputStream = new MemoryStream(Encoding.UTF8.GetBytes("buffered response")); + return new InvocationResponse(outputStream); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null)) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + Assert.False(streamingClient.StartStreamingResponseAsyncCalled); + Assert.True(streamingClient.SendResponseAsyncCalled); + } + + /// + /// Property 14: Exception After Writes Uses Trailers + /// When a handler throws an exception after writing data to an IResponseStream, + /// the error is reported via trailers (ReportErrorAsync) rather than standard error reporting. + /// **Validates: Requirements 5.6, 5.7** + /// + [Fact] + public async Task MidstreamError_ExceptionAfterWrites_ReportsViaTrailers() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + await stream.WriteAsync(Encoding.UTF8.GetBytes("partial data")); + throw new InvalidOperationException("midstream failure"); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null)) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + // Error should be reported via trailers on the stream, not via standard error reporting + Assert.True(streamingClient.StartStreamingResponseAsyncCalled); + Assert.NotNull(streamingClient.LastStreamingResponseStream); + Assert.True(streamingClient.LastStreamingResponseStream.HasError); + Assert.False(streamingClient.ReportInvocationErrorAsyncExceptionCalled); + } + + /// + /// Property 15: Exception Before CreateStream Uses Standard Error + /// When a handler throws an exception before calling ResponseStreamFactory.CreateStream(), + /// the error is reported using the standard Lambda error reporting mechanism. + /// **Validates: Requirements 5.7, 7.1** + /// + [Fact] + public async Task PreStreamError_ExceptionBeforeCreateStream_UsesStandardErrorReporting() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + throw new InvalidOperationException("pre-stream failure"); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null)) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + Assert.False(streamingClient.StartStreamingResponseAsyncCalled); + Assert.True(streamingClient.ReportInvocationErrorAsyncExceptionCalled); + } + + /// + /// State Isolation: ResponseStreamFactory state is cleared after each invocation. + /// **Validates: Requirements 6.5, 8.9** + /// + [Fact] + public async Task Cleanup_ResponseStreamFactoryStateCleared_AfterInvocation() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + await stream.WriteAsync(Encoding.UTF8.GetBytes("data")); + return new InvocationResponse(Stream.Null, false); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null)) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + // After invocation, factory state should be cleaned up + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(false)); + Assert.Null(ResponseStreamFactory.GetSendTask(false)); + } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs new file mode 100644 index 000000000..1128bb075 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Amazon.Lambda.RuntimeSupport.Helpers; +using Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + /// + /// A RuntimeApiClient subclass for testing LambdaBootstrap streaming integration. + /// Extends RuntimeApiClient so the (RuntimeApiClient)Client cast in LambdaBootstrap works. + /// Overrides StartStreamingResponseAsync to avoid real HTTP calls. + /// + internal class TestStreamingRuntimeApiClient : RuntimeApiClient, IRuntimeApiClient + { + private readonly IEnvironmentVariables _environmentVariables; + private readonly Dictionary> _headers; + + public new IConsoleLoggerWriter ConsoleLogger { get; } = new LogLevelLoggerWriter(new SystemEnvironmentVariables()); + + public TestStreamingRuntimeApiClient(IEnvironmentVariables environmentVariables, Dictionary> headers) + : base(environmentVariables, new NoOpInternalRuntimeApiClient()) + { + _environmentVariables = environmentVariables; + _headers = headers; + } + + // Tracking flags + public bool GetNextInvocationAsyncCalled { get; private set; } + public bool ReportInitializationErrorAsyncExceptionCalled { get; private set; } + public bool ReportInvocationErrorAsyncExceptionCalled { get; private set; } + public bool SendResponseAsyncCalled { get; private set; } + public bool StartStreamingResponseAsyncCalled { get; private set; } + + public string LastTraceId { get; private set; } + public byte[] FunctionInput { get; set; } + public Stream LastOutputStream { get; private set; } + public Exception LastRecordedException { get; private set; } + public ResponseStream LastStreamingResponseStream { get; private set; } + + public new async Task GetNextInvocationAsync(CancellationToken cancellationToken = default) + { + GetNextInvocationAsyncCalled = true; + + LastTraceId = Guid.NewGuid().ToString(); + _headers[RuntimeApiHeaders.HeaderTraceId] = new List() { LastTraceId }; + + var inputStream = new MemoryStream(FunctionInput == null ? new byte[0] : FunctionInput); + inputStream.Position = 0; + + return new InvocationRequest() + { + InputStream = inputStream, + LambdaContext = new LambdaContext( + new RuntimeApiHeaders(_headers), + new LambdaEnvironment(_environmentVariables), + new TestDateTimeHelper(), new SimpleLoggerWriter(_environmentVariables)) + }; + } + + public new Task ReportInitializationErrorAsync(Exception exception, String errorType = null, CancellationToken cancellationToken = default) + { + LastRecordedException = exception; + ReportInitializationErrorAsyncExceptionCalled = true; + return Task.CompletedTask; + } + + public new Task ReportInitializationErrorAsync(string errorType, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public new Task ReportInvocationErrorAsync(string awsRequestId, Exception exception, CancellationToken cancellationToken = default) + { + LastRecordedException = exception; + ReportInvocationErrorAsyncExceptionCalled = true; + return Task.CompletedTask; + } + + public new async Task SendResponseAsync(string awsRequestId, Stream outputStream, CancellationToken cancellationToken = default) + { + if (outputStream != null) + { + LastOutputStream = new MemoryStream((int)outputStream.Length); + outputStream.CopyTo(LastOutputStream); + LastOutputStream.Position = 0; + } + + SendResponseAsyncCalled = true; + } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + StartStreamingResponseAsyncCalled = true; + LastStreamingResponseStream = responseStream; + + // Simulate the HTTP stream being available + responseStream.SetHttpOutputStream(new MemoryStream()); + + // Wait for the handler to finish writing (mirrors real SerializeToStreamAsync behavior) + await responseStream.WaitForCompletionAsync(); + } + +#if NET8_0_OR_GREATER + public new Task RestoreNextInvocationAsync(CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public new Task ReportRestoreErrorAsync(Exception exception, String errorType = null, CancellationToken cancellationToken = default) + => Task.CompletedTask; +#endif + } +} From 0b8dd52562168558f4013a5ac868e9afb907387e Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 18 Feb 2026 15:53:22 -0800 Subject: [PATCH 10/13] Task 10 --- .../HandlerTests.cs | 2 +- .../LambdaBootstrapTests.cs | 1 + .../ResponseStreamFactoryTests.cs | 1 + .../StreamingIntegrationTests.cs | 652 ++++++++++++++++++ 4 files changed, 655 insertions(+), 1 deletion(-) create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingIntegrationTests.cs diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs index 80f9d13d0..e257b688e 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs @@ -31,7 +31,7 @@ namespace Amazon.Lambda.RuntimeSupport.UnitTests { - [Collection("Bootstrap")] + [Collection("ResponseStreamFactory")] public class HandlerTests { private const string AggregateExceptionTestMarker = "AggregateExceptionTesting"; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs index 07c2379a0..ce922d529 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs @@ -31,6 +31,7 @@ namespace Amazon.Lambda.RuntimeSupport.UnitTests /// Tests to test LambdaBootstrap when it's constructed using its actual constructor. /// Tests of the static GetLambdaBootstrap methods can be found in LambdaBootstrapWrapperTests. /// + [Collection("ResponseStreamFactory")] public class LambdaBootstrapTests { readonly TestHandler _testFunction; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs index 11973ae5f..1c714dd97 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs @@ -20,6 +20,7 @@ namespace Amazon.Lambda.RuntimeSupport.UnitTests { + [Collection("ResponseStreamFactory")] public class ResponseStreamFactoryTests : IDisposable { private const long MaxResponseSize = 20 * 1024 * 1024; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingIntegrationTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingIntegrationTests.cs new file mode 100644 index 000000000..c2bd34bdf --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingIntegrationTests.cs @@ -0,0 +1,652 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + [CollectionDefinition("ResponseStreamFactory")] + public class ResponseStreamFactoryCollection { } + + /// + /// End-to-end integration tests for the true-streaming architecture. + /// These tests exercise the full pipeline: LambdaBootstrap → ResponseStreamFactory → + /// ResponseStream → StreamingHttpContent → captured HTTP output stream. + /// + [Collection("ResponseStreamFactory")] + public class StreamingIntegrationTests : IDisposable + { + public void Dispose() + { + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + } + + // ─── Helpers ──────────────────────────────────────────────────────────────── + + private static Dictionary> MakeHeaders(string requestId = "test-request-id") + => new Dictionary> + { + { RuntimeApiHeaders.HeaderAwsRequestId, new List { requestId } }, + { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List { "arn:aws:lambda:us-east-1:123456789012:function:test" } }, + { RuntimeApiHeaders.HeaderAwsTenantId, new List { "tenant-id" } }, + { RuntimeApiHeaders.HeaderTraceId, new List { "trace-id" } }, + { RuntimeApiHeaders.HeaderDeadlineMs, new List { "9999999999999" } }, + }; + + /// + /// A capturing RuntimeApiClient that records the raw bytes written to the HTTP output stream + /// by SerializeToStreamAsync, enabling assertions on chunked-encoding format. + /// + private class CapturingStreamingRuntimeApiClient : RuntimeApiClient, IRuntimeApiClient + { + private readonly IEnvironmentVariables _envVars; + private readonly Dictionary> _headers; + + public bool StartStreamingCalled { get; private set; } + public bool SendResponseCalled { get; private set; } + public bool ReportInvocationErrorCalled { get; private set; } + public byte[] CapturedHttpBytes { get; private set; } + public ResponseStream LastResponseStream { get; private set; } + public Stream LastBufferedOutputStream { get; private set; } + + public new Amazon.Lambda.RuntimeSupport.Helpers.IConsoleLoggerWriter ConsoleLogger { get; } = new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables()); + + public CapturingStreamingRuntimeApiClient( + IEnvironmentVariables envVars, + Dictionary> headers) + : base(envVars, new NoOpInternalRuntimeApiClient()) + { + _envVars = envVars; + _headers = headers; + } + + public new async Task GetNextInvocationAsync(CancellationToken cancellationToken = default) + { + _headers[RuntimeApiHeaders.HeaderTraceId] = new List { Guid.NewGuid().ToString() }; + var inputStream = new MemoryStream(new byte[0]); + return new InvocationRequest + { + InputStream = inputStream, + LambdaContext = new LambdaContext( + new RuntimeApiHeaders(_headers), + new LambdaEnvironment(_envVars), + new TestDateTimeHelper(), + new Helpers.SimpleLoggerWriter(_envVars)) + }; + } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + StartStreamingCalled = true; + LastResponseStream = responseStream; + + // Use a real MemoryStream as the HTTP output stream so we capture actual bytes + var captureStream = new MemoryStream(); + var content = new StreamingHttpContent(responseStream); + + // SerializeToStreamAsync hands the stream to ResponseStream and waits for completion + await content.CopyToAsync(captureStream); + CapturedHttpBytes = captureStream.ToArray(); + } + + public new async Task SendResponseAsync(string awsRequestId, Stream outputStream, CancellationToken cancellationToken = default) + { + SendResponseCalled = true; + if (outputStream != null) + { + var ms = new MemoryStream(); + await outputStream.CopyToAsync(ms); + ms.Position = 0; + LastBufferedOutputStream = ms; + } + } + + public new Task ReportInvocationErrorAsync(string awsRequestId, Exception exception, CancellationToken cancellationToken = default) + { + ReportInvocationErrorCalled = true; + return Task.CompletedTask; + } + + public new Task ReportInitializationErrorAsync(Exception exception, string errorType = null, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public new Task ReportInitializationErrorAsync(string errorType, CancellationToken cancellationToken = default) + => Task.CompletedTask; + +#if NET8_0_OR_GREATER + public new Task RestoreNextInvocationAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; + public new Task ReportRestoreErrorAsync(Exception exception, string errorType = null, CancellationToken cancellationToken = default) => Task.CompletedTask; +#endif + } + + private static CapturingStreamingRuntimeApiClient CreateClient(string requestId = "test-request-id") + => new CapturingStreamingRuntimeApiClient(new TestEnvironmentVariables(), MakeHeaders(requestId)); + + // ─── 10.1 End-to-end streaming response ───────────────────────────────────── + + /// + /// End-to-end: handler calls CreateStream, writes multiple chunks. + /// Verifies data flows through with correct chunked encoding and stream is finalized. + /// Requirements: 3.2, 4.3, 10.1 + /// + [Fact] + public async Task Streaming_MultipleChunks_FlowThroughWithChunkedEncoding() + { + var client = CreateClient(); + var chunks = new[] { "Hello", ", ", "World" }; + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + foreach (var chunk in chunks) + await stream.WriteAsync(Encoding.UTF8.GetBytes(chunk)); + return new InvocationResponse(Stream.Null, false); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.StartStreamingCalled); + Assert.NotNull(client.CapturedHttpBytes); + + var output = Encoding.UTF8.GetString(client.CapturedHttpBytes); + + // Each chunk should appear as: hex-size\r\ndata\r\n + Assert.Contains("5\r\nHello\r\n", output); + Assert.Contains("2\r\n, \r\n", output); + Assert.Contains("5\r\nWorld\r\n", output); + + // Final chunk terminates the stream + Assert.Contains("0\r\n", output); + Assert.EndsWith("0\r\n\r\n", output); + } + + /// + /// End-to-end: all data is transmitted correctly (content round-trip). + /// Requirements: 3.2, 4.3, 10.1 + /// + [Fact] + public async Task Streaming_AllDataTransmitted_ContentRoundTrip() + { + var client = CreateClient(); + var payload = Encoding.UTF8.GetBytes("integration test payload"); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + await stream.WriteAsync(payload); + return new InvocationResponse(Stream.Null, false); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + var output = client.CapturedHttpBytes; + Assert.NotNull(output); + + // Decode the single chunk: hex-size\r\ndata\r\n + var outputStr = Encoding.UTF8.GetString(output); + var hexSize = payload.Length.ToString("X"); + Assert.Contains(hexSize + "\r\n", outputStr); + Assert.Contains("integration test payload", outputStr); + } + + /// + /// End-to-end: stream is finalized (final chunk written, BytesWritten matches). + /// Requirements: 3.2, 4.3, 10.1 + /// + [Fact] + public async Task Streaming_StreamFinalized_BytesWrittenMatchesPayload() + { + var client = CreateClient(); + var data = Encoding.UTF8.GetBytes("finalization check"); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + await stream.WriteAsync(data); + return new InvocationResponse(Stream.Null, false); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.NotNull(client.LastResponseStream); + Assert.Equal(data.Length, client.LastResponseStream.BytesWritten); + Assert.True(client.LastResponseStream.IsCompleted); + } + + // ─── 10.2 End-to-end buffered response ────────────────────────────────────── + + /// + /// End-to-end: handler does NOT call CreateStream — response goes via buffered path. + /// Verifies SendResponseAsync is called and streaming headers are absent. + /// Requirements: 1.5, 4.6, 9.4 + /// + [Fact] + public async Task Buffered_HandlerDoesNotCallCreateStream_UsesSendResponsePath() + { + var client = CreateClient(); + var responseBody = Encoding.UTF8.GetBytes("buffered response body"); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + return new InvocationResponse(new MemoryStream(responseBody)); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.False(client.StartStreamingCalled, "StartStreamingResponseAsync should NOT be called for buffered mode"); + Assert.True(client.SendResponseCalled, "SendResponseAsync should be called for buffered mode"); + Assert.Null(client.CapturedHttpBytes); + } + + /// + /// End-to-end: buffered response body is transmitted correctly. + /// Requirements: 1.5, 4.6, 9.4 + /// + [Fact] + public async Task Buffered_ResponseBodyTransmittedCorrectly() + { + var client = CreateClient(); + var responseBody = Encoding.UTF8.GetBytes("hello buffered world"); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + return new InvocationResponse(new MemoryStream(responseBody)); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.SendResponseCalled); + Assert.NotNull(client.LastBufferedOutputStream); + var received = new MemoryStream(); + await client.LastBufferedOutputStream.CopyToAsync(received); + Assert.Equal(responseBody, received.ToArray()); + } + + // ─── 10.3 Midstream error ──────────────────────────────────────────────────── + + /// + /// End-to-end: handler writes data then throws — error trailers appear after final chunk. + /// Requirements: 5.1, 5.2, 5.3, 5.6 + /// + [Fact] + public async Task MidstreamError_ErrorTrailersIncludedAfterFinalChunk() + { + var client = CreateClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + await stream.WriteAsync(Encoding.UTF8.GetBytes("partial data")); + throw new InvalidOperationException("midstream failure"); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.StartStreamingCalled); + Assert.NotNull(client.CapturedHttpBytes); + + var output = Encoding.UTF8.GetString(client.CapturedHttpBytes); + + // Data chunk should be present + Assert.Contains("partial data", output); + + // Final chunk must appear + Assert.Contains("0\r\n", output); + + // Error trailers must appear after the final chunk + var finalChunkIdx = output.LastIndexOf("0\r\n"); + var errorTypeIdx = output.IndexOf(StreamingConstants.ErrorTypeTrailer + ":"); + var errorBodyIdx = output.IndexOf(StreamingConstants.ErrorBodyTrailer + ":"); + + Assert.True(errorTypeIdx > finalChunkIdx, "Error-Type trailer should appear after final chunk"); + Assert.True(errorBodyIdx > finalChunkIdx, "Error-Body trailer should appear after final chunk"); + + // Error type should reference the exception type + Assert.Contains("InvalidOperationException", output); + + // Standard error reporting should NOT be used (error went via trailers) + Assert.False(client.ReportInvocationErrorCalled); + } + + /// + /// End-to-end: handler throws before writing any data — standard error reporting is used. + /// Requirements: 5.6, 5.7 + /// + [Fact] + public async Task PreStreamError_ExceptionBeforeAnyWrite_UsesStandardErrorReporting() + { + var client = CreateClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + // Throw before writing anything + throw new ArgumentException("pre-write failure"); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + // BytesWritten == 0, so standard error reporting should be used + Assert.True(client.ReportInvocationErrorCalled, + "Standard error reporting should be used when no bytes were written"); + } + + /// + /// End-to-end: error body trailer contains JSON with exception details. + /// Requirements: 5.2, 5.3 + /// + [Fact] + public async Task MidstreamError_ErrorBodyTrailerContainsJsonDetails() + { + var client = CreateClient(); + const string errorMessage = "something went wrong mid-stream"; + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(); + await stream.WriteAsync(Encoding.UTF8.GetBytes("some data")); + throw new InvalidOperationException(errorMessage); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + var output = Encoding.UTF8.GetString(client.CapturedHttpBytes); + Assert.Contains(StreamingConstants.ErrorBodyTrailer + ":", output); + Assert.Contains(errorMessage, output); + } + + // ─── 10.4 Multi-concurrency ────────────────────────────────────────────────── + + /// + /// Multi-concurrency: concurrent invocations use AsyncLocal for state isolation. + /// Each invocation independently uses streaming or buffered mode without interference. + /// Requirements: 2.9, 6.5, 8.9 + /// + [Fact] + public async Task MultiConcurrency_ConcurrentInvocations_StateIsolated() + { + const int concurrency = 3; + var results = new ConcurrentDictionary(); + var barrier = new SemaphoreSlim(0, concurrency); + var allStarted = new SemaphoreSlim(0, concurrency); + + // Simulate concurrent invocations using AsyncLocal directly + var tasks = new List(); + for (int i = 0; i < concurrency; i++) + { + var requestId = $"req-{i}"; + var payload = $"payload-{i}"; + tasks.Add(Task.Run(async () => + { + var mockClient = new MockMultiConcurrencyStreamingClient(); + ResponseStreamFactory.InitializeInvocation( + requestId, + StreamingConstants.MaxResponseSize, + isMultiConcurrency: true, + mockClient, + CancellationToken.None); + + var stream = ResponseStreamFactory.CreateStream(); + allStarted.Release(); + + // Wait until all tasks have started (to ensure true concurrency) + await barrier.WaitAsync(); + + await stream.WriteAsync(Encoding.UTF8.GetBytes(payload)); + ((ResponseStream)stream).MarkCompleted(); + + // Verify this invocation's stream is still accessible + var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + results[requestId] = retrieved != null ? payload : "MISSING"; + + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + })); + } + + // Wait for all tasks to start, then release the barrier + for (int i = 0; i < concurrency; i++) + await allStarted.WaitAsync(); + barrier.Release(concurrency); + + await Task.WhenAll(tasks); + + // Each invocation should have seen its own stream + Assert.Equal(concurrency, results.Count); + for (int i = 0; i < concurrency; i++) + Assert.Equal($"payload-{i}", results[$"req-{i}"]); + } + + /// + /// Multi-concurrency: streaming and buffered invocations can run concurrently without interference. + /// Requirements: 2.9, 6.5, 8.9 + /// + [Fact] + public async Task MultiConcurrency_StreamingAndBufferedMixedConcurrently_NoInterference() + { + var streamingResults = new ConcurrentBag(); + var bufferedResults = new ConcurrentBag(); + var barrier = new SemaphoreSlim(0, 4); + var allStarted = new SemaphoreSlim(0, 4); + + var tasks = new List(); + + // 2 streaming invocations + for (int i = 0; i < 2; i++) + { + var requestId = $"stream-{i}"; + tasks.Add(Task.Run(async () => + { + var mockClient = new MockMultiConcurrencyStreamingClient(); + ResponseStreamFactory.InitializeInvocation( + requestId, StreamingConstants.MaxResponseSize, + isMultiConcurrency: true, mockClient, CancellationToken.None); + + var stream = ResponseStreamFactory.CreateStream(); + allStarted.Release(); + await barrier.WaitAsync(); + + await stream.WriteAsync(Encoding.UTF8.GetBytes("streaming data")); + ((ResponseStream)stream).MarkCompleted(); + + var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + streamingResults.Add(retrieved != null); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + })); + } + + // 2 buffered invocations (no CreateStream) + for (int i = 0; i < 2; i++) + { + var requestId = $"buffered-{i}"; + tasks.Add(Task.Run(async () => + { + var mockClient = new MockMultiConcurrencyStreamingClient(); + ResponseStreamFactory.InitializeInvocation( + requestId, StreamingConstants.MaxResponseSize, + isMultiConcurrency: true, mockClient, CancellationToken.None); + + allStarted.Release(); + await barrier.WaitAsync(); + + // No CreateStream — buffered mode + var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + bufferedResults.Add(retrieved == null); // should be null (no stream created) + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + })); + } + + for (int i = 0; i < 4; i++) + await allStarted.WaitAsync(); + barrier.Release(4); + + await Task.WhenAll(tasks); + + Assert.Equal(2, streamingResults.Count); + Assert.All(streamingResults, r => Assert.True(r, "Streaming invocation should have a stream")); + + Assert.Equal(2, bufferedResults.Count); + Assert.All(bufferedResults, r => Assert.True(r, "Buffered invocation should have no stream")); + } + + /// + /// Minimal mock RuntimeApiClient for multi-concurrency tests. + /// Accepts StartStreamingResponseAsync calls without real HTTP. + /// + private class MockMultiConcurrencyStreamingClient : RuntimeApiClient + { + public MockMultiConcurrencyStreamingClient() + : base(new TestEnvironmentVariables(), new NoOpInternalRuntimeApiClient()) { } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + // Provide the HTTP output stream so writes don't block + responseStream.SetHttpOutputStream(new MemoryStream()); + await responseStream.WaitForCompletionAsync(); + } + } + + // ─── 10.5 Backward compatibility ──────────────────────────────────────────── + + /// + /// Backward compatibility: existing handler signatures (event + ILambdaContext) work without modification. + /// Requirements: 9.1, 9.2, 9.3 + /// + [Fact] + public async Task BackwardCompat_ExistingHandlerSignature_WorksUnchanged() + { + var client = CreateClient(); + bool handlerCalled = false; + + // Simulate a classic handler that returns a buffered response + LambdaBootstrapHandler handler = async (invocation) => + { + handlerCalled = true; + await Task.Yield(); + return new InvocationResponse(new MemoryStream(Encoding.UTF8.GetBytes("classic response"))); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(handlerCalled); + Assert.True(client.SendResponseCalled); + Assert.False(client.StartStreamingCalled); + } + + /// + /// Backward compatibility: no regression in buffered response behavior — response body is correct. + /// Requirements: 9.4, 9.5 + /// + [Fact] + public async Task BackwardCompat_BufferedResponse_NoRegression() + { + var client = CreateClient(); + var expected = Encoding.UTF8.GetBytes("no regression here"); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + return new InvocationResponse(new MemoryStream(expected)); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.SendResponseCalled); + Assert.NotNull(client.LastBufferedOutputStream); + var received = new MemoryStream(); + await client.LastBufferedOutputStream.CopyToAsync(received); + Assert.Equal(expected, received.ToArray()); + } + + /// + /// Backward compatibility: handler that returns null OutputStream still works. + /// Requirements: 9.4 + /// + [Fact] + public async Task BackwardCompat_NullOutputStream_HandledGracefully() + { + var client = CreateClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + return new InvocationResponse(Stream.Null, false); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + + // Should not throw + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.SendResponseCalled); + } + + /// + /// Backward compatibility: handler that throws before CreateStream uses standard error path. + /// Requirements: 9.5 + /// + [Fact] + public async Task BackwardCompat_HandlerThrows_StandardErrorReportingUsed() + { + var client = CreateClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + throw new Exception("classic handler error"); + }; + + using var bootstrap = new LambdaBootstrap(handler, null); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.ReportInvocationErrorCalled); + Assert.False(client.StartStreamingCalled); + } + } +} From 414a4495eed950d9cbad9dcf8e24aff19797af3e Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 18 Feb 2026 17:47:09 -0800 Subject: [PATCH 11/13] Refactoring --- .../Bootstrap/LambdaBootstrap.cs | 11 +-- ...onseStream.cs => ILambdaResponseStream.cs} | 14 +-- .../Client/InvocationResponse.cs | 26 ----- ...daResponseStream.ILambdaResponseStream.cs} | 78 ++++++++++----- .../Client/LambdaResponseStream.Stream.cs | 97 +++++++++++++++++++ ...text.cs => LambdaResponseStreamContext.cs} | 9 +- ...tory.cs => LambdaResponseStreamFactory.cs} | 30 +++--- .../Client/RuntimeApiClient.cs | 2 +- .../Client/StreamingConstants.cs | 5 - .../Client/StreamingHttpContent.cs | 4 +- .../InvocationResponseTests.cs | 81 ---------------- .../LambdaBootstrapTests.cs | 10 +- .../ResponseStreamFactoryTests.cs | 74 +++++++------- .../ResponseStreamTests.cs | 45 +++------ .../RuntimeApiClientTests.cs | 18 ++-- .../StreamingHttpContentTests.cs | 30 +++--- .../StreamingIntegrationTests.cs | 53 +++++----- .../TestStreamingRuntimeApiClient.cs | 4 +- 18 files changed, 282 insertions(+), 309 deletions(-) rename Libraries/src/Amazon.Lambda.RuntimeSupport/Client/{IResponseStream.cs => ILambdaResponseStream.cs} (77%) rename Libraries/src/Amazon.Lambda.RuntimeSupport/Client/{ResponseStream.cs => LambdaResponseStream.ILambdaResponseStream.cs} (65%) create mode 100644 Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStream.Stream.cs rename Libraries/src/Amazon.Lambda.RuntimeSupport/Client/{ResponseStreamContext.cs => LambdaResponseStreamContext.cs} (88%) rename Libraries/src/Amazon.Lambda.RuntimeSupport/Client/{ResponseStreamFactory.cs => LambdaResponseStreamFactory.cs} (79%) delete mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/InvocationResponseTests.cs diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs index 68b67c339..6241fb61f 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -363,9 +363,8 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul var runtimeApiClient = Client as RuntimeApiClient; if (runtimeApiClient != null) { - ResponseStreamFactory.InitializeInvocation( + LambdaResponseStreamFactory.InitializeInvocation( invocation.LambdaContext.AwsRequestId, - StreamingConstants.MaxResponseSize, isMultiConcurrency, runtimeApiClient, cancellationToken); @@ -386,7 +385,7 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul { WriteUnhandledExceptionToLog(exception); - var streamIfCreated = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency); + var streamIfCreated = LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency); if (streamIfCreated != null && streamIfCreated.BytesWritten > 0) { // Midstream error — report via trailers on the already-open HTTP connection @@ -404,10 +403,10 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul } // If streaming was started, await the HTTP send task to ensure it completes - var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency); + var sendTask = LambdaResponseStreamFactory.GetSendTask(isMultiConcurrency); if (sendTask != null) { - var stream = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency); + var stream = LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency); if (stream != null && !stream.IsCompleted && !stream.HasError) { // Handler returned successfully — signal stream completion @@ -454,7 +453,7 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul { if (runtimeApiClient != null) { - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency); } invocation.Dispose(); } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IResponseStream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ILambdaResponseStream.cs similarity index 77% rename from Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IResponseStream.cs rename to Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ILambdaResponseStream.cs index 6107dde16..36236b28d 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IResponseStream.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ILambdaResponseStream.cs @@ -23,7 +23,7 @@ namespace Amazon.Lambda.RuntimeSupport /// Interface for writing streaming responses in AWS Lambda functions. /// Obtained by calling ResponseStreamFactory.CreateStream() within a handler. /// - public interface IResponseStream : IDisposable + public interface ILambdaResponseStream : IDisposable { /// /// Asynchronously writes a byte array to the response stream. @@ -32,7 +32,6 @@ public interface IResponseStream : IDisposable /// Optional cancellation token. /// A task representing the asynchronous operation. /// Thrown if the stream is already completed or an error has been reported. - /// Thrown if writing would exceed the 20 MiB limit. Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default); /// @@ -44,19 +43,8 @@ public interface IResponseStream : IDisposable /// Optional cancellation token. /// A task representing the asynchronous operation. /// Thrown if the stream is already completed or an error has been reported. - /// Thrown if writing would exceed the 20 MiB limit. Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default); - /// - /// Asynchronously writes a memory buffer to the response stream. - /// - /// The memory buffer to write. - /// Optional cancellation token. - /// A task representing the asynchronous operation. - /// Thrown if the stream is already completed or an error has been reported. - /// Thrown if writing would exceed the 20 MiB limit. - Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default); - /// /// Reports an error that occurred during streaming. /// This will send error information via HTTP trailing headers. diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationResponse.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationResponse.cs index 4438c9708..1894b0521 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationResponse.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InvocationResponse.cs @@ -34,18 +34,6 @@ public class InvocationResponse /// public bool DisposeOutputStream { get; private set; } = true; - /// - /// Indicates whether this response uses streaming mode. - /// Set internally by the runtime when ResponseStreamFactory.CreateStream() is called. - /// - internal bool IsStreaming { get; set; } - - /// - /// The ResponseStream instance if streaming mode is used. - /// Set internally by the runtime. - /// - internal ResponseStream ResponseStream { get; set; } - /// /// Construct a InvocationResponse with an output stream that will be disposed by the Lambda Runtime Client. /// @@ -64,20 +52,6 @@ public InvocationResponse(Stream outputStream, bool disposeOutputStream) { OutputStream = outputStream ?? throw new ArgumentNullException(nameof(outputStream)); DisposeOutputStream = disposeOutputStream; - IsStreaming = false; - } - - /// - /// Creates an InvocationResponse for a streaming response. - /// Used internally by the runtime. - /// - internal static InvocationResponse CreateStreamingResponse(ResponseStream responseStream) - { - return new InvocationResponse(Stream.Null, false) - { - IsStreaming = true, - ResponseStream = responseStream - }; } } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStream.ILambdaResponseStream.cs similarity index 65% rename from Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs rename to Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStream.ILambdaResponseStream.cs index 00f63cf75..7830c81b4 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStream.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStream.ILambdaResponseStream.cs @@ -22,14 +22,13 @@ namespace Amazon.Lambda.RuntimeSupport { /// - /// Internal implementation of IResponseStream with true streaming. - /// Writes data directly to the HTTP output stream as chunked transfer encoding. + /// A write-only, non-seekable subclass that streams response data + /// to the Lambda Runtime API. Returned by . /// - internal class ResponseStream : IResponseStream + public partial class LambdaResponseStream : Stream, ILambdaResponseStream { private static readonly byte[] CrlfBytes = Encoding.ASCII.GetBytes("\r\n"); - private readonly long _maxResponseSize; private long _bytesWritten; private bool _isCompleted; private bool _hasError; @@ -41,14 +40,26 @@ internal class ResponseStream : IResponseStream private readonly SemaphoreSlim _httpStreamReady = new SemaphoreSlim(0, 1); private readonly SemaphoreSlim _completionSignal = new SemaphoreSlim(0, 1); + /// + /// The number of bytes written to the Lambda response stream so far. + /// public long BytesWritten => _bytesWritten; + + /// + /// Gets a value indicating whether the operation has completed. + /// public bool IsCompleted => _isCompleted; + + /// + /// Gets a value indicating whether an error has occurred. + /// public bool HasError => _hasError; + + internal Exception ReportedError => _reportedError; - public ResponseStream(long maxResponseSize) + internal LambdaResponseStream() { - _maxResponseSize = maxResponseSize; } /// @@ -69,6 +80,13 @@ internal async Task WaitForCompletionAsync() await _completionSignal.WaitAsync(); } + /// + /// Asynchronously writes a byte array to the response stream. + /// + /// The byte array to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. public async Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default) { if (buffer == null) @@ -77,7 +95,16 @@ public async Task WriteAsync(byte[] buffer, CancellationToken cancellationToken await WriteAsync(buffer, 0, buffer.Length, cancellationToken); } - public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + /// + /// Asynchronously writes a portion of a byte array to the response stream. + /// + /// The byte array containing data to write. + /// The zero-based byte offset in buffer at which to begin copying bytes. + /// The number of bytes to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); @@ -93,14 +120,6 @@ public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationT lock (_lock) { ThrowIfCompletedOrError(); - - if (_bytesWritten + count > _maxResponseSize) - { - throw new InvalidOperationException( - $"Writing {count} bytes would exceed the maximum response size of {_maxResponseSize} bytes (20 MiB). " + - $"Current size: {_bytesWritten} bytes."); - } - _bytesWritten += count; } @@ -120,13 +139,14 @@ public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationT } } - public async Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - // Convert to array and delegate — small overhead but keeps the API simple - var array = buffer.ToArray(); - await WriteAsync(array, 0, array.Length, cancellationToken); - } - + /// + /// Reports an error that occurred during streaming. + /// This will send error information via HTTP trailing headers. + /// + /// The exception to report. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has already been reported. public Task ReportErrorAsync(Exception exception, CancellationToken cancellationToken = default) { if (exception == null) @@ -145,6 +165,7 @@ public Task ReportErrorAsync(Exception exception, CancellationToken cancellation // Signal completion so StreamingHttpContent can write error trailers and finish _completionSignal.Release(); + return Task.CompletedTask; } @@ -166,10 +187,17 @@ private void ThrowIfCompletedOrError() throw new InvalidOperationException("Cannot write to a stream after an error has been reported."); } - public void Dispose() + // ── Dispose ────────────────────────────────────────────────────────── + + /// + protected override void Dispose(bool disposing) { - // Ensure completion is signaled if not already - try { _completionSignal.Release(); } catch (SemaphoreFullException) { } + if (disposing) + { + try { _completionSignal.Release(); } catch (SemaphoreFullException) { } + } + + base.Dispose(disposing); } } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStream.Stream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStream.Stream.cs new file mode 100644 index 000000000..5453333e7 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStream.Stream.cs @@ -0,0 +1,97 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// A write-only, non-seekable subclass that streams response data + /// to the Lambda Runtime API. Returned by . + /// Integrates with standard .NET stream consumers such as . + /// + public partial class LambdaResponseStream : Stream, ILambdaResponseStream + { + // ── System.IO.Stream — capabilities ───────────────────────────────── + + /// Gets a value indicating whether the stream supports reading. Always false. + public override bool CanRead => false; + + /// Gets a value indicating whether the stream supports seeking. Always false. + public override bool CanSeek => false; + + /// Gets a value indicating whether the stream supports writing. Always true. + public override bool CanWrite => true; + + // ── System.IO.Stream — Length / Position ──────────────────────────── + + /// + /// Gets the total number of bytes written to the stream so far. + /// Equivalent to . + /// + public override long Length => BytesWritten; + + /// + /// Getting or setting the position is not supported. + /// + /// Always thrown. + public override long Position + { + get => throw new NotSupportedException("LambdaResponseStream does not support seeking."); + set => throw new NotSupportedException("LambdaResponseStream does not support seeking."); + } + + // ── System.IO.Stream — seek / read (not supported) ────────────────── + + /// Not supported. + /// Always thrown. + public override long Seek(long offset, SeekOrigin origin) + => throw new NotImplementedException("LambdaResponseStream does not support seeking."); + + /// Not supported. + /// Always thrown. + public override int Read(byte[] buffer, int offset, int count) + => throw new NotImplementedException("LambdaResponseStream does not support reading."); + + /// Not supported. + /// Always thrown. + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new NotImplementedException("LambdaResponseStream does not support reading."); + + // ── System.IO.Stream — write ───────────────────────────────────────── + + /// + /// Writes a sequence of bytes to the stream. Delegates to the async path synchronously. + /// Prefer to avoid blocking. + /// + public override void Write(byte[] buffer, int offset, int count) + => WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + + // ── System.IO.Stream — flush / set length ──────────────────────────── + + /// + /// Flush is a no-op; data is sent to the Runtime API immediately on each write. + /// + public override void Flush() { } + + /// Not supported. + /// Always thrown. + public override void SetLength(long value) + => throw new NotSupportedException("LambdaResponseStream does not support SetLength."); + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStreamContext.cs similarity index 88% rename from Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs rename to Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStreamContext.cs index dc0b4a629..c6a58c81d 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStreamContext.cs @@ -21,18 +21,13 @@ namespace Amazon.Lambda.RuntimeSupport /// /// Internal context class used by ResponseStreamFactory to track per-invocation streaming state. /// - internal class ResponseStreamContext + internal class LambdaResponseStreamContext { /// /// The AWS request ID for the current invocation. /// public string AwsRequestId { get; set; } - /// - /// Maximum allowed response size in bytes (20 MiB). - /// - public long MaxResponseSize { get; set; } - /// /// Whether CreateStream() has been called for this invocation. /// @@ -41,7 +36,7 @@ internal class ResponseStreamContext /// /// The ResponseStream instance if created. /// - public ResponseStream Stream { get; set; } + public LambdaResponseStream Stream { get; set; } /// /// The RuntimeApiClient used to start the streaming HTTP POST. diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStreamFactory.cs similarity index 79% rename from Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs rename to Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStreamFactory.cs index 613980fb1..84d8c0ebd 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ResponseStreamFactory.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/LambdaResponseStreamFactory.cs @@ -23,22 +23,25 @@ namespace Amazon.Lambda.RuntimeSupport /// Factory for creating streaming responses in AWS Lambda functions. /// Call CreateStream() within your handler to opt into response streaming for that invocation. /// - public static class ResponseStreamFactory + public static class LambdaResponseStreamFactory { // For on-demand mode (single invocation at a time) - private static ResponseStreamContext _onDemandContext; + private static LambdaResponseStreamContext _onDemandContext; // For multi-concurrency mode (multiple concurrent invocations) - private static readonly AsyncLocal _asyncLocalContext = new AsyncLocal(); + private static readonly AsyncLocal _asyncLocalContext = new AsyncLocal(); /// /// Creates a streaming response for the current invocation. /// Can only be called once per invocation. /// - /// An IResponseStream for writing response data. + /// + /// A — a subclass — for writing + /// response data. The returned stream also implements . + /// /// Thrown if called outside an invocation context. /// Thrown if called more than once per invocation. - public static IResponseStream CreateStream() + public static LambdaResponseStream CreateStream() { var context = GetCurrentContext(); @@ -54,29 +57,28 @@ public static IResponseStream CreateStream() "ResponseStreamFactory.CreateStream() can only be called once per invocation."); } - var stream = new ResponseStream(context.MaxResponseSize); - context.Stream = stream; + var lambdaStream = new LambdaResponseStream(); + context.Stream = lambdaStream; context.StreamCreated = true; // Start the HTTP POST to the Runtime API. // This runs concurrently — SerializeToStreamAsync will block // until the handler finishes writing or reports an error. context.SendTask = context.RuntimeApiClient.StartStreamingResponseAsync( - context.AwsRequestId, stream, context.CancellationToken); + context.AwsRequestId, lambdaStream, context.CancellationToken); - return stream; + return lambdaStream; } // Internal methods for LambdaBootstrap to manage state internal static void InitializeInvocation( - string awsRequestId, long maxResponseSize, bool isMultiConcurrency, + string awsRequestId, bool isMultiConcurrency, RuntimeApiClient runtimeApiClient, CancellationToken cancellationToken) { - var context = new ResponseStreamContext + var context = new LambdaResponseStreamContext { AwsRequestId = awsRequestId, - MaxResponseSize = maxResponseSize, StreamCreated = false, Stream = null, RuntimeApiClient = runtimeApiClient, @@ -93,7 +95,7 @@ internal static void InitializeInvocation( } } - internal static ResponseStream GetStreamIfCreated(bool isMultiConcurrency) + internal static LambdaResponseStream GetStreamIfCreated(bool isMultiConcurrency) { var context = isMultiConcurrency ? _asyncLocalContext.Value : _onDemandContext; return context?.Stream; @@ -121,7 +123,7 @@ internal static void CleanupInvocation(bool isMultiConcurrency) } } - private static ResponseStreamContext GetCurrentContext() + private static LambdaResponseStreamContext GetCurrentContext() { // Check multi-concurrency first (AsyncLocal), then on-demand return _asyncLocalContext.Value ?? _onDemandContext; diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs index 13c4e4eac..f594d5e56 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs @@ -189,7 +189,7 @@ public Task ReportRestoreErrorAsync(Exception exception, String errorType = null /// The optional cancellation token to use. /// A Task representing the in-flight HTTP POST. internal virtual async Task StartStreamingResponseAsync( - string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + string awsRequestId, LambdaResponseStream responseStream, CancellationToken cancellationToken = default) { if (awsRequestId == null) throw new ArgumentNullException(nameof(awsRequestId)); if (responseStream == null) throw new ArgumentNullException(nameof(responseStream)); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingConstants.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingConstants.cs index 7eeec86a2..c1e99ed17 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingConstants.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingConstants.cs @@ -20,11 +20,6 @@ namespace Amazon.Lambda.RuntimeSupport /// internal static class StreamingConstants { - /// - /// Maximum response size for Lambda streaming responses: 20 MiB. - /// - public const long MaxResponseSize = 20 * 1024 * 1024; - /// /// Header name for Lambda response mode. /// diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs index e563d343b..c642873aa 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/StreamingHttpContent.cs @@ -30,9 +30,9 @@ internal class StreamingHttpContent : HttpContent private static readonly byte[] CrlfBytes = Encoding.ASCII.GetBytes("\r\n"); private static readonly byte[] FinalChunkBytes = Encoding.ASCII.GetBytes("0\r\n"); - private readonly ResponseStream _responseStream; + private readonly LambdaResponseStream _responseStream; - public StreamingHttpContent(ResponseStream responseStream) + public StreamingHttpContent(LambdaResponseStream responseStream) { _responseStream = responseStream ?? throw new ArgumentNullException(nameof(responseStream)); } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/InvocationResponseTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/InvocationResponseTests.cs deleted file mode 100644 index 703ac0cd9..000000000 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/InvocationResponseTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -using System.IO; -using Xunit; - -namespace Amazon.Lambda.RuntimeSupport.UnitTests -{ - public class InvocationResponseTests - { - private const long MaxResponseSize = 20 * 1024 * 1024; - - /// - /// Property 17: InvocationResponse Streaming Flag - Existing constructors set IsStreaming to false. - /// Validates: Requirements 7.3, 8.1 - /// - [Fact] - public void Constructor_WithStream_IsStreamingIsFalse() - { - var response = new InvocationResponse(new MemoryStream()); - - Assert.False(response.IsStreaming); - Assert.Null(response.ResponseStream); - } - - [Fact] - public void Constructor_WithStreamAndDispose_IsStreamingIsFalse() - { - var response = new InvocationResponse(new MemoryStream(), false); - - Assert.False(response.IsStreaming); - Assert.Null(response.ResponseStream); - } - - /// - /// Property 17: InvocationResponse Streaming Flag - CreateStreamingResponse sets IsStreaming to true. - /// Validates: Requirements 7.3, 8.1 - /// - [Fact] - public void CreateStreamingResponse_SetsIsStreamingTrue() - { - var stream = new ResponseStream(MaxResponseSize); - - var response = InvocationResponse.CreateStreamingResponse(stream); - - Assert.True(response.IsStreaming); - } - - [Fact] - public void CreateStreamingResponse_SetsResponseStream() - { - var stream = new ResponseStream(MaxResponseSize); - - var response = InvocationResponse.CreateStreamingResponse(stream); - - Assert.Same(stream, response.ResponseStream); - } - - [Fact] - public void CreateStreamingResponse_DoesNotDisposeOutputStream() - { - var stream = new ResponseStream(MaxResponseSize); - - var response = InvocationResponse.CreateStreamingResponse(stream); - - Assert.False(response.DisposeOutputStream); - } - } -} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs index ce922d529..ae40b7e2e 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs @@ -314,7 +314,7 @@ public async Task StreamingMode_HandlerCallsCreateStream_SendTaskAwaited() LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); await stream.WriteAsync(Encoding.UTF8.GetBytes("hello")); return new InvocationResponse(Stream.Null, false); }; @@ -369,7 +369,7 @@ public async Task MidstreamError_ExceptionAfterWrites_ReportsViaTrailers() LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); await stream.WriteAsync(Encoding.UTF8.GetBytes("partial data")); throw new InvalidOperationException("midstream failure"); }; @@ -425,7 +425,7 @@ public async Task Cleanup_ResponseStreamFactoryStateCleared_AfterInvocation() LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); await stream.WriteAsync(Encoding.UTF8.GetBytes("data")); return new InvocationResponse(Stream.Null, false); }; @@ -437,8 +437,8 @@ public async Task Cleanup_ResponseStreamFactoryStateCleared_AfterInvocation() } // After invocation, factory state should be cleaned up - Assert.Null(ResponseStreamFactory.GetStreamIfCreated(false)); - Assert.Null(ResponseStreamFactory.GetSendTask(false)); + Assert.Null(LambdaResponseStreamFactory.GetStreamIfCreated(false)); + Assert.Null(LambdaResponseStreamFactory.GetSendTask(false)); } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs index 1c714dd97..9fce99ad5 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs @@ -28,8 +28,8 @@ public class ResponseStreamFactoryTests : IDisposable public void Dispose() { // Clean up both modes to avoid test pollution - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); } /// @@ -40,7 +40,7 @@ private class MockStreamingRuntimeApiClient : RuntimeApiClient { public bool StartStreamingCalled { get; private set; } public string LastAwsRequestId { get; private set; } - public ResponseStream LastResponseStream { get; private set; } + public LambdaResponseStream LastResponseStream { get; private set; } public TaskCompletionSource SendTaskCompletion { get; } = new TaskCompletionSource(); public MockStreamingRuntimeApiClient() @@ -49,7 +49,7 @@ public MockStreamingRuntimeApiClient() } internal override async Task StartStreamingResponseAsync( - string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + string awsRequestId, LambdaResponseStream responseStream, CancellationToken cancellationToken = default) { StartStreamingCalled = true; LastAwsRequestId = awsRequestId; @@ -60,8 +60,8 @@ internal override async Task StartStreamingResponseAsync( private void InitializeWithMock(string requestId, bool isMultiConcurrency, MockStreamingRuntimeApiClient mockClient) { - ResponseStreamFactory.InitializeInvocation( - requestId, MaxResponseSize, isMultiConcurrency, + LambdaResponseStreamFactory.InitializeInvocation( + requestId, isMultiConcurrency, mockClient, CancellationToken.None); } @@ -77,10 +77,10 @@ public void CreateStream_OnDemandMode_ReturnsValidStream() var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-1", isMultiConcurrency: false, mock); - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); Assert.NotNull(stream); - Assert.IsAssignableFrom(stream); + Assert.IsAssignableFrom(stream); } /// @@ -93,10 +93,10 @@ public void CreateStream_MultiConcurrencyMode_ReturnsValidStream() var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-2", isMultiConcurrency: true, mock); - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); Assert.NotNull(stream); - Assert.IsAssignableFrom(stream); + Assert.IsAssignableFrom(stream); } // --- Property 4: Single Stream Per Invocation --- @@ -110,16 +110,16 @@ public void CreateStream_CalledTwice_ThrowsInvalidOperationException() { var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-3", isMultiConcurrency: false, mock); - ResponseStreamFactory.CreateStream(); + LambdaResponseStreamFactory.CreateStream(); - Assert.Throws(() => ResponseStreamFactory.CreateStream()); + Assert.Throws(() => LambdaResponseStreamFactory.CreateStream()); } [Fact] public void CreateStream_OutsideInvocationContext_ThrowsInvalidOperationException() { // No InitializeInvocation called - Assert.Throws(() => ResponseStreamFactory.CreateStream()); + Assert.Throws(() => LambdaResponseStreamFactory.CreateStream()); } // --- CreateStream starts HTTP POST --- @@ -134,7 +134,7 @@ public void CreateStream_CallsStartStreamingResponseAsync() var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-start", isMultiConcurrency: false, mock); - ResponseStreamFactory.CreateStream(); + LambdaResponseStreamFactory.CreateStream(); Assert.True(mock.StartStreamingCalled); Assert.Equal("req-start", mock.LastAwsRequestId); @@ -153,9 +153,9 @@ public void GetSendTask_AfterCreateStream_ReturnsNonNullTask() var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-send", isMultiConcurrency: false, mock); - ResponseStreamFactory.CreateStream(); + LambdaResponseStreamFactory.CreateStream(); - var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency: false); + var sendTask = LambdaResponseStreamFactory.GetSendTask(isMultiConcurrency: false); Assert.NotNull(sendTask); } @@ -165,14 +165,14 @@ public void GetSendTask_BeforeCreateStream_ReturnsNull() var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-nosend", isMultiConcurrency: false, mock); - var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency: false); + var sendTask = LambdaResponseStreamFactory.GetSendTask(isMultiConcurrency: false); Assert.Null(sendTask); } [Fact] public void GetSendTask_NoContext_ReturnsNull() { - Assert.Null(ResponseStreamFactory.GetSendTask(isMultiConcurrency: false)); + Assert.Null(LambdaResponseStreamFactory.GetSendTask(isMultiConcurrency: false)); } // --- Internal methods --- @@ -183,9 +183,9 @@ public void InitializeInvocation_OnDemand_SetsUpContext() var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-4", isMultiConcurrency: false, mock); - Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + Assert.Null(LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); Assert.NotNull(stream); } @@ -195,9 +195,9 @@ public void InitializeInvocation_MultiConcurrency_SetsUpContext() var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-5", isMultiConcurrency: true, mock); - Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true)); + Assert.Null(LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true)); - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); Assert.NotNull(stream); } @@ -206,16 +206,16 @@ public void GetStreamIfCreated_AfterCreateStream_ReturnsStream() { var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-6", isMultiConcurrency: false, mock); - ResponseStreamFactory.CreateStream(); + LambdaResponseStreamFactory.CreateStream(); - var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false); + var retrieved = LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false); Assert.NotNull(retrieved); } [Fact] public void GetStreamIfCreated_NoContext_ReturnsNull() { - Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + Assert.Null(LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); } [Fact] @@ -223,12 +223,12 @@ public void CleanupInvocation_ClearsState() { var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-7", isMultiConcurrency: false, mock); - ResponseStreamFactory.CreateStream(); + LambdaResponseStreamFactory.CreateStream(); - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); - Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); - Assert.Throws(() => ResponseStreamFactory.CreateStream()); + Assert.Null(LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + Assert.Throws(() => LambdaResponseStreamFactory.CreateStream()); } // --- Property 16: State Isolation Between Invocations --- @@ -244,17 +244,17 @@ public void StateIsolation_SequentialInvocations_NoLeakage() // First invocation - streaming InitializeWithMock("req-8a", isMultiConcurrency: false, mock); - var stream1 = ResponseStreamFactory.CreateStream(); + var stream1 = LambdaResponseStreamFactory.CreateStream(); Assert.NotNull(stream1); - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); // Second invocation - should start fresh InitializeWithMock("req-8b", isMultiConcurrency: false, mock); - Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + Assert.Null(LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); - var stream2 = ResponseStreamFactory.CreateStream(); + var stream2 = LambdaResponseStreamFactory.CreateStream(); Assert.NotNull(stream2); - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); } /// @@ -266,14 +266,14 @@ public async Task StateIsolation_MultiConcurrency_UsesAsyncLocal() { var mock = new MockStreamingRuntimeApiClient(); InitializeWithMock("req-9", isMultiConcurrency: true, mock); - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); Assert.NotNull(stream); bool childSawNull = false; await Task.Run(() => { - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); - childSawNull = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true) == null; + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + childSawNull = LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true) == null; }); Assert.True(childSawNull); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs index a6ef2fe6f..735fba482 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs @@ -25,15 +25,13 @@ namespace Amazon.Lambda.RuntimeSupport.UnitTests { public class ResponseStreamTests { - private const long MaxResponseSize = 20 * 1024 * 1024; // 20 MiB - /// /// Helper: creates a ResponseStream and wires up a MemoryStream as the HTTP output stream. /// Returns both so tests can inspect what was written. /// - private static (ResponseStream stream, MemoryStream httpOutput) CreateWiredStream(long maxSize = MaxResponseSize) + private static (LambdaResponseStream stream, MemoryStream httpOutput) CreateWiredStream() { - var rs = new ResponseStream(maxSize); + var rs = new LambdaResponseStream(); var output = new MemoryStream(); rs.SetHttpOutputStream(output); return (rs, output); @@ -44,7 +42,7 @@ private static (ResponseStream stream, MemoryStream httpOutput) CreateWiredStrea [Fact] public void Constructor_InitializesStateCorrectly() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); Assert.Equal(0, stream.BytesWritten); Assert.False(stream.IsCompleted); @@ -100,27 +98,6 @@ public async Task WriteAsync_WithOffset_WritesCorrectSliceAsChunk() Assert.Equal(expected, written); } - /// - /// Property 9: Chunked Encoding Format — ReadOnlyMemory overload. - /// Validates: Requirements 3.2, 10.1 - /// - [Fact] - public async Task WriteAsync_ReadOnlyMemory_WritesChunkedFormat() - { - var (stream, httpOutput) = CreateWiredStream(); - var data = new ReadOnlyMemory(new byte[] { 10, 20, 30 }); - - await stream.WriteAsync(data); - - var written = httpOutput.ToArray(); - var expected = Encoding.ASCII.GetBytes("3\r\n") - .Concat(new byte[] { 10, 20, 30 }) - .Concat(Encoding.ASCII.GetBytes("\r\n")) - .ToArray(); - - Assert.Equal(expected, written); - } - // ---- Property 5: Written Data Appears in HTTP Response Immediately ---- /// @@ -170,7 +147,7 @@ public async Task WriteAsync_LargerPayload_HexSizeIsCorrect() [Fact] public async Task WriteAsync_BlocksUntilSetHttpOutputStream() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var httpOutput = new MemoryStream(); var writeStarted = new ManualResetEventSlim(false); var writeCompleted = new ManualResetEventSlim(false); @@ -277,7 +254,7 @@ public async Task SizeLimit_ExactlyAtLimit_Succeeds() await stream.WriteAsync(data); - Assert.Equal(MaxResponseSize, stream.BytesWritten); + Assert.Equal(data.Length, stream.BytesWritten); } // ---- Property 19: Writes After Completion Rejected ---- @@ -317,7 +294,7 @@ await Assert.ThrowsAsync( [Fact] public async Task ReportErrorAsync_SetsErrorState() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var exception = new InvalidOperationException("something broke"); await stream.ReportErrorAsync(exception); @@ -329,7 +306,7 @@ public async Task ReportErrorAsync_SetsErrorState() [Fact] public async Task ReportErrorAsync_AfterCompleted_Throws() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); stream.MarkCompleted(); await Assert.ThrowsAsync( @@ -339,7 +316,7 @@ await Assert.ThrowsAsync( [Fact] public async Task ReportErrorAsync_CalledTwice_Throws() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); await stream.ReportErrorAsync(new Exception("first")); await Assert.ThrowsAsync( @@ -349,7 +326,7 @@ await Assert.ThrowsAsync( [Fact] public void MarkCompleted_SetsCompletionState() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); stream.MarkCompleted(); @@ -377,7 +354,7 @@ public async Task WriteAsync_NullBufferWithOffset_ThrowsArgumentNull() [Fact] public async Task ReportErrorAsync_NullException_ThrowsArgumentNull() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); await Assert.ThrowsAsync(() => stream.ReportErrorAsync(null)); } @@ -387,7 +364,7 @@ public async Task ReportErrorAsync_NullException_ThrowsArgumentNull() [Fact] public async Task Dispose_ReleasesCompletionSignalIfNotAlreadyReleased() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var waitTask = stream.WaitForCompletionAsync(); Assert.False(waitTask.IsCompleted); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs index 75abec101..fbc4a8ae6 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs @@ -40,9 +40,9 @@ public class RuntimeApiClientTests private class MockHttpMessageHandler : HttpMessageHandler { public HttpRequestMessage CapturedRequest { get; private set; } - private readonly ResponseStream _responseStream; + private readonly LambdaResponseStream _responseStream; - public MockHttpMessageHandler(ResponseStream responseStream) + public MockHttpMessageHandler(LambdaResponseStream responseStream) { _responseStream = responseStream; } @@ -57,7 +57,7 @@ protected override Task SendAsync( } private static RuntimeApiClient CreateClientWithMockHandler( - ResponseStream stream, out MockHttpMessageHandler handler) + LambdaResponseStream stream, out MockHttpMessageHandler handler) { handler = new MockHttpMessageHandler(stream); var httpClient = new HttpClient(handler); @@ -77,7 +77,7 @@ private static RuntimeApiClient CreateClientWithMockHandler( [Fact] public async Task StartStreamingResponseAsync_IncludesStreamingResponseModeHeader() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var client = CreateClientWithMockHandler(stream, out var handler); await client.StartStreamingResponseAsync("req-1", stream, CancellationToken.None); @@ -100,7 +100,7 @@ public async Task StartStreamingResponseAsync_IncludesStreamingResponseModeHeade [Fact] public async Task StartStreamingResponseAsync_IncludesChunkedTransferEncodingHeader() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var client = CreateClientWithMockHandler(stream, out var handler); await client.StartStreamingResponseAsync("req-2", stream, CancellationToken.None); @@ -121,7 +121,7 @@ public async Task StartStreamingResponseAsync_IncludesChunkedTransferEncodingHea [Fact] public async Task StartStreamingResponseAsync_DeclaresTrailerHeaderUpfront() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var client = CreateClientWithMockHandler(stream, out var handler); await client.StartStreamingResponseAsync("req-3", stream, CancellationToken.None); @@ -144,7 +144,7 @@ public async Task StartStreamingResponseAsync_DeclaresTrailerHeaderUpfront() [Fact] public async Task StartStreamingResponseAsync_MarksStreamCompletedAfterSuccess() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var client = CreateClientWithMockHandler(stream, out _); await client.StartStreamingResponseAsync("req-4", stream, CancellationToken.None); @@ -203,7 +203,7 @@ public async Task SendResponseAsync_BufferedResponse_ExcludesStreamingHeaders() [Fact] public async Task StartStreamingResponseAsync_NullRequestId_ThrowsArgumentNullException() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var client = CreateClientWithMockHandler(stream, out _); await Assert.ThrowsAsync( @@ -213,7 +213,7 @@ await Assert.ThrowsAsync( [Fact] public async Task StartStreamingResponseAsync_NullResponseStream_ThrowsArgumentNullException() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var client = CreateClientWithMockHandler(stream, out _); await Assert.ThrowsAsync( diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs index 53b1e88b7..1f85f47a8 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingHttpContentTests.cs @@ -32,8 +32,8 @@ public class StreamingHttpContentTests /// Returns the bytes written to the HTTP output stream. /// private async Task SerializeWithConcurrentHandler( - ResponseStream responseStream, - Func handlerAction) + LambdaResponseStream responseStream, + Func handlerAction) { var content = new StreamingHttpContent(responseStream); var outputStream = new MemoryStream(); @@ -63,7 +63,7 @@ private async Task SerializeWithConcurrentHandler( [Fact] public async Task SerializeToStreamAsync_HandsOffHttpStream_WritesFlowThrough() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { @@ -84,7 +84,7 @@ public async Task SerializeToStreamAsync_HandsOffHttpStream_WritesFlowThrough() [Fact] public async Task SerializeToStreamAsync_BlocksUntilMarkCompleted() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var content = new StreamingHttpContent(rs); var outputStream = new MemoryStream(); @@ -108,7 +108,7 @@ public async Task SerializeToStreamAsync_BlocksUntilMarkCompleted() [Fact] public async Task SerializeToStreamAsync_BlocksUntilReportErrorAsync() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var content = new StreamingHttpContent(rs); var outputStream = new MemoryStream(); @@ -132,7 +132,7 @@ public async Task SerializeToStreamAsync_BlocksUntilReportErrorAsync() [Fact] public async Task FinalChunk_WrittenAfterCompletion() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { @@ -156,7 +156,7 @@ public async Task FinalChunk_WrittenAfterCompletion() [Fact] public async Task FinalChunk_EmptyStream_StillWritten() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, stream => { @@ -177,7 +177,7 @@ public async Task FinalChunk_EmptyStream_StillWritten() [Fact] public async Task ErrorTrailers_AppearAfterFinalChunk() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { @@ -210,7 +210,7 @@ public async Task ErrorTrailers_AppearAfterFinalChunk() [InlineData(typeof(NullReferenceException))] public async Task ErrorTrailer_IncludesErrorType(Type exceptionType) { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { @@ -232,7 +232,7 @@ public async Task ErrorTrailer_IncludesErrorType(Type exceptionType) [Fact] public async Task ErrorTrailer_IncludesJsonErrorBody() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { @@ -255,7 +255,7 @@ public async Task ErrorTrailer_IncludesJsonErrorBody() [Fact] public async Task SuccessfulCompletion_EndsWithCrlf() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { @@ -275,7 +275,7 @@ public async Task SuccessfulCompletion_EndsWithCrlf() [Fact] public async Task ErrorCompletion_EndsWithCrlf() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { @@ -292,7 +292,7 @@ public async Task ErrorCompletion_EndsWithCrlf() [Fact] public async Task NoError_NoTrailersWritten() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { @@ -310,7 +310,7 @@ public async Task NoError_NoTrailersWritten() [Fact] public void TryComputeLength_ReturnsFalse() { - var stream = new ResponseStream(MaxResponseSize); + var stream = new LambdaResponseStream(); var content = new StreamingHttpContent(stream); var result = content.Headers.ContentLength; @@ -326,7 +326,7 @@ public void TryComputeLength_ReturnsFalse() [Fact] public async Task CrlfTerminators_NoBareLineFeed() { - var rs = new ResponseStream(MaxResponseSize); + var rs = new LambdaResponseStream(); var output = await SerializeWithConcurrentHandler(rs, async stream => { diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingIntegrationTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingIntegrationTests.cs index c2bd34bdf..0f15680f4 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingIntegrationTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingIntegrationTests.cs @@ -38,8 +38,8 @@ public class StreamingIntegrationTests : IDisposable { public void Dispose() { - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); } // ─── Helpers ──────────────────────────────────────────────────────────────── @@ -67,7 +67,7 @@ private class CapturingStreamingRuntimeApiClient : RuntimeApiClient, IRuntimeApi public bool SendResponseCalled { get; private set; } public bool ReportInvocationErrorCalled { get; private set; } public byte[] CapturedHttpBytes { get; private set; } - public ResponseStream LastResponseStream { get; private set; } + public LambdaResponseStream LastResponseStream { get; private set; } public Stream LastBufferedOutputStream { get; private set; } public new Amazon.Lambda.RuntimeSupport.Helpers.IConsoleLoggerWriter ConsoleLogger { get; } = new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables()); @@ -97,7 +97,7 @@ public CapturingStreamingRuntimeApiClient( } internal override async Task StartStreamingResponseAsync( - string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + string awsRequestId, LambdaResponseStream responseStream, CancellationToken cancellationToken = default) { StartStreamingCalled = true; LastResponseStream = responseStream; @@ -159,7 +159,7 @@ public async Task Streaming_MultipleChunks_FlowThroughWithChunkedEncoding() LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); foreach (var chunk in chunks) await stream.WriteAsync(Encoding.UTF8.GetBytes(chunk)); return new InvocationResponse(Stream.Null, false); @@ -196,7 +196,7 @@ public async Task Streaming_AllDataTransmitted_ContentRoundTrip() LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); await stream.WriteAsync(payload); return new InvocationResponse(Stream.Null, false); }; @@ -227,7 +227,7 @@ public async Task Streaming_StreamFinalized_BytesWrittenMatchesPayload() LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); await stream.WriteAsync(data); return new InvocationResponse(Stream.Null, false); }; @@ -309,7 +309,7 @@ public async Task MidstreamError_ErrorTrailersIncludedAfterFinalChunk() LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); await stream.WriteAsync(Encoding.UTF8.GetBytes("partial data")); throw new InvalidOperationException("midstream failure"); }; @@ -355,7 +355,7 @@ public async Task PreStreamError_ExceptionBeforeAnyWrite_UsesStandardErrorReport LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); // Throw before writing anything throw new ArgumentException("pre-write failure"); }; @@ -381,7 +381,7 @@ public async Task MidstreamError_ErrorBodyTrailerContainsJsonDetails() LambdaBootstrapHandler handler = async (invocation) => { - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); await stream.WriteAsync(Encoding.UTF8.GetBytes("some data")); throw new InvalidOperationException(errorMessage); }; @@ -419,27 +419,26 @@ public async Task MultiConcurrency_ConcurrentInvocations_StateIsolated() tasks.Add(Task.Run(async () => { var mockClient = new MockMultiConcurrencyStreamingClient(); - ResponseStreamFactory.InitializeInvocation( + LambdaResponseStreamFactory.InitializeInvocation( requestId, - StreamingConstants.MaxResponseSize, isMultiConcurrency: true, mockClient, CancellationToken.None); - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); allStarted.Release(); // Wait until all tasks have started (to ensure true concurrency) await barrier.WaitAsync(); await stream.WriteAsync(Encoding.UTF8.GetBytes(payload)); - ((ResponseStream)stream).MarkCompleted(); + stream.MarkCompleted(); // Verify this invocation's stream is still accessible - var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + var retrieved = LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); results[requestId] = retrieved != null ? payload : "MISSING"; - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); })); } @@ -477,20 +476,20 @@ public async Task MultiConcurrency_StreamingAndBufferedMixedConcurrently_NoInter tasks.Add(Task.Run(async () => { var mockClient = new MockMultiConcurrencyStreamingClient(); - ResponseStreamFactory.InitializeInvocation( - requestId, StreamingConstants.MaxResponseSize, + LambdaResponseStreamFactory.InitializeInvocation( + requestId, isMultiConcurrency: true, mockClient, CancellationToken.None); - var stream = ResponseStreamFactory.CreateStream(); + var stream = LambdaResponseStreamFactory.CreateStream(); allStarted.Release(); await barrier.WaitAsync(); await stream.WriteAsync(Encoding.UTF8.GetBytes("streaming data")); - ((ResponseStream)stream).MarkCompleted(); + stream.MarkCompleted(); - var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + var retrieved = LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); streamingResults.Add(retrieved != null); - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); })); } @@ -501,17 +500,17 @@ public async Task MultiConcurrency_StreamingAndBufferedMixedConcurrently_NoInter tasks.Add(Task.Run(async () => { var mockClient = new MockMultiConcurrencyStreamingClient(); - ResponseStreamFactory.InitializeInvocation( - requestId, StreamingConstants.MaxResponseSize, + LambdaResponseStreamFactory.InitializeInvocation( + requestId, isMultiConcurrency: true, mockClient, CancellationToken.None); allStarted.Release(); await barrier.WaitAsync(); // No CreateStream — buffered mode - var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + var retrieved = LambdaResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); bufferedResults.Add(retrieved == null); // should be null (no stream created) - ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + LambdaResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); })); } @@ -538,7 +537,7 @@ public MockMultiConcurrencyStreamingClient() : base(new TestEnvironmentVariables(), new NoOpInternalRuntimeApiClient()) { } internal override async Task StartStreamingResponseAsync( - string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + string awsRequestId, LambdaResponseStream responseStream, CancellationToken cancellationToken = default) { // Provide the HTTP output stream so writes don't block responseStream.SetHttpOutputStream(new MemoryStream()); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs index 1128bb075..da68d2940 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs @@ -54,7 +54,7 @@ public TestStreamingRuntimeApiClient(IEnvironmentVariables environmentVariables, public byte[] FunctionInput { get; set; } public Stream LastOutputStream { get; private set; } public Exception LastRecordedException { get; private set; } - public ResponseStream LastStreamingResponseStream { get; private set; } + public LambdaResponseStream LastStreamingResponseStream { get; private set; } public new async Task GetNextInvocationAsync(CancellationToken cancellationToken = default) { @@ -108,7 +108,7 @@ public TestStreamingRuntimeApiClient(IEnvironmentVariables environmentVariables, } internal override async Task StartStreamingResponseAsync( - string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + string awsRequestId, LambdaResponseStream responseStream, CancellationToken cancellationToken = default) { StartStreamingResponseAsyncCalled = true; LastStreamingResponseStream = responseStream; From 21d82d85116fd9f01fbfdc612c3be2cd5ae1b25e Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 18 Feb 2026 17:58:02 -0800 Subject: [PATCH 12/13] Cleanup --- .../Client/ILambdaResponseStream.cs | 2 +- .../serverless.template | 659 +++++++++- .../serverless.template | 22 +- .../TestServerlessApp/serverless.template | 1149 ++++++++++++++++- 4 files changed, 1826 insertions(+), 6 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ILambdaResponseStream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ILambdaResponseStream.cs index 36236b28d..d3565fdbc 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ILambdaResponseStream.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/ILambdaResponseStream.cs @@ -21,7 +21,7 @@ namespace Amazon.Lambda.RuntimeSupport { /// /// Interface for writing streaming responses in AWS Lambda functions. - /// Obtained by calling ResponseStreamFactory.CreateStream() within a handler. + /// Obtained by calling within a handler. /// public interface ILambdaResponseStream : IDisposable { diff --git a/Libraries/test/TestExecutableServerlessApp/serverless.template b/Libraries/test/TestExecutableServerlessApp/serverless.template index 229385aba..ac43959b7 100644 --- a/Libraries/test/TestExecutableServerlessApp/serverless.template +++ b/Libraries/test/TestExecutableServerlessApp/serverless.template @@ -1,7 +1,7 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.8.0.0).", + "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", "Parameters": { "ArchitectureTypeParameter": { "Type": "String", @@ -21,7 +21,662 @@ ] } }, - "Resources": {}, + "Resources": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "OkResponseWithHeader" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheader/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2async/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1async/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "DynamicReturn" + } + } + } + }, + "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "DynamicInput" + } + } + } + }, + "GreeterSayHello": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 1024, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "SayHello" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHello", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "GreeterSayHelloAsync": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 50, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "SayHelloAsync" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHelloAsync", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "HasIntrinsic" + } + } + } + }, + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/nullableheaderhttpapi", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppParameterlessMethodsNoParameterGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NoParameter" + } + } + } + }, + "TestServerlessAppParameterlessMethodWithResponseNoParameterWithResponseGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NoParameterWithResponse" + } + } + } + }, + "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "GetPerson" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/", + "Method": "GET" + } + } + } + } + }, + "ToUpper": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "ToUpper" + } + } + } + }, + "ToLower": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "ToLower" + } + } + } + }, + "TestServerlessAppTaskExampleTaskReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "TaskReturn" + } + } + } + }, + "TestServerlessAppVoidExampleVoidReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "VoidReturn" + } + } + } + } + }, "Outputs": { "RestApiURL": { "Description": "Rest API endpoint URL for Prod environment", diff --git a/Libraries/test/TestServerlessApp.NET8/serverless.template b/Libraries/test/TestServerlessApp.NET8/serverless.template index 67ec5dfa4..c42ff4a47 100644 --- a/Libraries/test/TestServerlessApp.NET8/serverless.template +++ b/Libraries/test/TestServerlessApp.NET8/serverless.template @@ -1,6 +1,24 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.8.0.0).", - "Resources": {} + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", + "Resources": { + "TestServerlessAppNET8FunctionsToUpperGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp.NET8::TestServerlessApp.NET8.Functions_ToUpper_Generated::ToUpper" + } + } + } } \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index e6c1b8bea..0e3befbe1 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -1,7 +1,7 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.8.0.0).", + "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", "Parameters": { "ArchitectureTypeParameter": { "Type": "String", @@ -28,6 +28,1153 @@ "Resources": { "TestQueue": { "Type": "AWS::SQS::Queue" + }, + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" + ] + } + } + }, + "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" + ] + } + } + }, + "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/{text}", + "Method": "GET" + } + } + } + } + }, + "HttpApiAuthorizerTest": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer", + "Method": "GET" + } + } + } + } + }, + "SimpleCalculatorAdd": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Add", + "Method": "GET" + } + } + } + } + }, + "SimpleCalculatorSubtract": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Subtract", + "Method": "GET" + } + } + } + } + }, + "SimpleCalculatorMultiply": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Multiply/{x}/{y}", + "Method": "GET" + } + } + } + } + }, + "SimpleCalculatorDivideAsync": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", + "Method": "GET" + } + } + } + } + }, + "PI": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" + ] + } + } + }, + "Random": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" + ] + } + } + }, + "Randoms": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" + ] + } + } + }, + "SQSMessageHandler": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "TestQueueEvent" + ], + "SyncedEventProperties": { + "TestQueueEvent": [ + "Queue.Fn::GetAtt", + "BatchSize", + "FilterCriteria.Filters", + "FunctionResponseTypes", + "MaximumBatchingWindowInSeconds", + "ScalingConfig.MaximumConcurrency" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaSQSQueueExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" + ] + }, + "Events": { + "TestQueueEvent": { + "Type": "SQS", + "Properties": { + "BatchSize": 50, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" + } + ] + }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "MaximumBatchingWindowInSeconds": 5, + "ScalingConfig": { + "MaximumConcurrency": 5 + }, + "Queue": { + "Fn::GetAtt": [ + "TestQueue", + "Arn" + ] + } + } + } + } + } + }, + "HttpApiV1AuthorizerTest": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-v1", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/nullableheaderhttpapi", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/authorizerihttpresults", + "Method": "GET" + } + } + } + } + }, + "GreeterSayHello": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 1024, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHello", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "GreeterSayHelloAsync": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 50, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHelloAsync", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheader/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2async/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1async/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" + ] + } + } + }, + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" + ] + } + } + }, + "HttpApiNonString": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-non-string", + "Method": "GET" + } + } + } + } + }, + "AuthNameFallbackTest": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-fallback", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppVoidExampleVoidReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" + ] + } + } + }, + "RestAuthorizerTest": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/rest/authorizer", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppTaskExampleTaskReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" + ] + } + } + }, + "ToUpper": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" + ] + } + } + }, + "TestServerlessAppComplexCalculatorAddGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootPost" + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" + ] + }, + "Events": { + "RootPost": { + "Type": "HttpApi", + "Properties": { + "Path": "/ComplexCalculator/Add", + "Method": "POST" + } + } + } + } + }, + "TestServerlessAppComplexCalculatorSubtractGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootPost" + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" + ] + }, + "Events": { + "RootPost": { + "Type": "HttpApi", + "Properties": { + "Path": "/ComplexCalculator/Subtract", + "Method": "POST" + } + } + } + } } }, "Outputs": { From 556b7262472ba70f31c693c45ecf9f42200da582 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 18 Feb 2026 23:41:19 -0800 Subject: [PATCH 13/13] Remove tests --- .../c27a62e6-91ca-4a59-9406-394866cdfa62.json | 11 +++++ .../ResponseStreamTests.cs | 41 ------------------- 2 files changed, 11 insertions(+), 41 deletions(-) create mode 100644 .autover/changes/c27a62e6-91ca-4a59-9406-394866cdfa62.json diff --git a/.autover/changes/c27a62e6-91ca-4a59-9406-394866cdfa62.json b/.autover/changes/c27a62e6-91ca-4a59-9406-394866cdfa62.json new file mode 100644 index 000000000..9ad5afe6e --- /dev/null +++ b/.autover/changes/c27a62e6-91ca-4a59-9406-394866cdfa62.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Minor", + "ChangelogMessages": [ + "Add response streaming support" + ] + } + ] +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs index 735fba482..a4d265228 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs @@ -216,47 +216,6 @@ public async Task ReportErrorAsync_ReleasesCompletionSignal() Assert.True(stream.HasError); } - // ---- Property 6: Size Limit Enforcement ---- - - /// - /// Property 6: Size Limit Enforcement — single write exceeding limit throws. - /// Validates: Requirements 3.6, 3.7 - /// - [Theory] - [InlineData(21 * 1024 * 1024)] - public async Task SizeLimit_SingleWriteExceedingLimit_Throws(int writeSize) - { - var (stream, _) = CreateWiredStream(); - var data = new byte[writeSize]; - - await Assert.ThrowsAsync(() => stream.WriteAsync(data)); - } - - /// - /// Property 6: Size Limit Enforcement — multiple writes exceeding limit throws. - /// Validates: Requirements 3.6, 3.7 - /// - [Fact] - public async Task SizeLimit_MultipleWritesExceedingLimit_Throws() - { - var (stream, _) = CreateWiredStream(); - - await stream.WriteAsync(new byte[10 * 1024 * 1024]); - await Assert.ThrowsAsync( - () => stream.WriteAsync(new byte[11 * 1024 * 1024])); - } - - [Fact] - public async Task SizeLimit_ExactlyAtLimit_Succeeds() - { - var (stream, _) = CreateWiredStream(); - var data = new byte[20 * 1024 * 1024]; - - await stream.WriteAsync(data); - - Assert.Equal(data.Length, stream.BytesWritten); - } - // ---- Property 19: Writes After Completion Rejected ---- ///