Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="..\..\..\buildtools\common.props" />

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0;net11.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<Version>1.14.3</Version>
<Description>Provides a bootstrap and Lambda Runtime API Client to help you to develop custom .NET Core Lambda Runtimes.</Description>
<AssemblyTitle>Amazon.Lambda.RuntimeSupport</AssemblyTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="..\..\..\buildtools\common.props" />

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0;net11.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<Version>1.0.1</Version>
<Description>Provides a Restore Hooks library to help you register before snapshot and after restore hooks.</Description>
<AssemblyTitle>SnapshotRestore.Registry</AssemblyTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,28 +128,6 @@ public async Task StreamingErrorEndpoint_StreamIsTruncated()
}
}

[Fact]
public async Task OnCompletedCallback_IsExecuted()
{
var apiUrl = await _fixture.GetApiUrlAsync();
using var httpClient = new HttpClient();

var response = await httpClient.GetWithRetryAsync($"{apiUrl}oncompleted-test");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Output.WriteLine($"Body: {body}");
Assert.Contains("OnCompleted callback registered", body);

var verifyResponse = await httpClient.GetWithRetryAsync($"{apiUrl}oncompleted-verify");
Assert.Equal(HttpStatusCode.OK, verifyResponse.StatusCode);
var verifyBody = await verifyResponse.Content.ReadAsStringAsync();
Output.WriteLine($"Verify body: {verifyBody}");

var doc = JsonDocument.Parse(verifyBody);
Assert.True(doc.RootElement.GetProperty("onCompletedExecuted").GetBoolean(),
"OnCompleted callback should have been executed");
}

[Fact]
public async Task CustomHeaders_PassedThrough()
{
Expand Down Expand Up @@ -198,8 +176,9 @@ public async Task PostWithBody_EchoesRequestBody()
var apiUrl = await _fixture.GetApiUrlAsync();
using var httpClient = new HttpClient();

var content = new StringContent("Hello from integration test", Encoding.UTF8, "text/plain");
var response = await httpClient.PostAsync($"{apiUrl}echo-body", content);
var response = await httpClient.PostWithRetryAsync(
$"{apiUrl}echo-body",
new StringContent("Hello from integration test", Encoding.UTF8, "text/plain"));

Output.WriteLine($"Status: {response.StatusCode}");
var body = await response.Content.ReadAsStringAsync();
Expand Down Expand Up @@ -401,7 +380,7 @@ await s3Client.PutBucketAsync(new Amazon.S3.Model.PutBucketRequest
private async Task WaitForEndpointAsync()
{
using var httpClient = new HttpClient();
var maxRetries = 10;
var maxRetries = 20;
for (var i = 0; i < maxRetries; i++)
{
try
Expand All @@ -427,13 +406,49 @@ public static async Task<HttpResponseMessage> GetWithRetryAsync(
this HttpClient httpClient, string url,
HttpStatusCode expectedCode = HttpStatusCode.OK,
int maxRetries = 5, int delaySeconds = 5)
{
return await GetWithRetryAsync(httpClient, url, expectedCode, null, maxRetries, delaySeconds);
}

public static async Task<HttpResponseMessage> GetWithRetryAsync(
this HttpClient httpClient, string url,
HttpStatusCode expectedCode,
Func<HttpResponseMessage, Task<bool>> contentValidator,
int maxRetries = 5, int delaySeconds = 5)
{
for (var i = 0; i < maxRetries; i++)
{
try
{
var response = await httpClient.GetAsync(url);
if (response.StatusCode == expectedCode)
{
if (contentValidator == null || await contentValidator(response))
{
return response;
}
}
}
catch
{
// Ignore and retry
}
await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
}
throw new Exception($"Failed to get expected response from {url} after {maxRetries} attempts");
}

public static async Task<HttpResponseMessage> PostWithRetryAsync(
this HttpClient httpClient, string url, HttpContent content,
HttpStatusCode expectedCode = HttpStatusCode.OK,
int maxRetries = 5, int delaySeconds = 5)
{
for (var i = 0; i < maxRetries; i++)
{
try
{
var response = await httpClient.PostAsync(url, content);
if (response.StatusCode == expectedCode)
{
return response;
}
Expand All @@ -442,9 +457,22 @@ public static async Task<HttpResponseMessage> GetWithRetryAsync(
{
// Ignore and retry
}
// HttpContent can only be consumed once; create fresh content for retries
content = await CloneHttpContentAsync(content);
await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
}
throw new Exception($"Failed to get expected status code {expectedCode} from {url} after {maxRetries} attempts");
}

private static async Task<HttpContent> CloneHttpContentAsync(HttpContent original)
{
var bytes = await original.ReadAsByteArrayAsync();
var clone = new ByteArrayContent(bytes);
if (original.Headers.ContentType != null)
{
clone.Headers.ContentType = original.Headers.ContentType;
}
return clone;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

namespace Amazon.Lambda.RuntimeSupport.UnitTests
{
[Collection("ResponseStreamFactory")]
[Collection("RuntimeSupportStateCheck")]
public class HandlerTests
{
private const string AggregateExceptionTestMarker = "AggregateExceptionTesting";
Expand Down Expand Up @@ -285,6 +285,7 @@ await Record.ExceptionAsync(async () =>
private async Task<string> InvokeAsync(LambdaBootstrap bootstrap, string dataIn, TestRuntimeApiClient testRuntimeApiClient)
{
testRuntimeApiClient.FunctionInput = dataIn != null ? Encoding.UTF8.GetBytes(dataIn) : new byte[0];
testRuntimeApiClient.LastOutputStream = null;

using (var cancellationTokenSource = new CancellationTokenSource())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

namespace Amazon.Lambda.RuntimeSupport.UnitTests
{
[Collection("RuntimeSupportStateCheck")]
public class HandlerWrapperTests
{
private static readonly JsonSerializer Serializer = new JsonSerializer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +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.
/// </summary>
[Collection("ResponseStreamFactory")]
[Collection("RuntimeSupportStateCheck")]
public class LambdaBootstrapTests
{
readonly TestHandler _testFunction;
Expand Down Expand Up @@ -319,7 +319,7 @@ public async Task StreamingMode_HandlerCallsCreateStream_SendTaskAwaited()
return new InvocationResponse(Stream.Null, false);
};

using (var bootstrap = new LambdaBootstrap(handler, null))
using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()))
{
bootstrap.Client = streamingClient;
await bootstrap.InvokeOnceAsync();
Expand All @@ -346,7 +346,7 @@ public async Task BufferedMode_HandlerDoesNotCallCreateStream_UsesSendResponse()
return new InvocationResponse(outputStream);
};

using (var bootstrap = new LambdaBootstrap(handler, null))
using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()))
{
bootstrap.Client = streamingClient;
await bootstrap.InvokeOnceAsync();
Expand Down Expand Up @@ -374,7 +374,7 @@ public async Task MidstreamError_ExceptionAfterWrites_ReportsViaTrailers()
throw new InvalidOperationException("midstream failure");
};

using (var bootstrap = new LambdaBootstrap(handler, null))
using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()))
{
bootstrap.Client = streamingClient;
await bootstrap.InvokeOnceAsync();
Expand Down Expand Up @@ -404,7 +404,7 @@ public async Task PreStreamError_ExceptionBeforeCreateStream_UsesStandardErrorRe
throw new InvalidOperationException("pre-stream failure");
};

using (var bootstrap = new LambdaBootstrap(handler, null))
using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()))
{
bootstrap.Client = streamingClient;
await bootstrap.InvokeOnceAsync();
Expand All @@ -430,7 +430,7 @@ public async Task Cleanup_ResponseStreamFactoryStateCleared_AfterInvocation()
return new InvocationResponse(Stream.Null, false);
};

using (var bootstrap = new LambdaBootstrap(handler, null))
using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()))
{
bootstrap.Client = streamingClient;
await bootstrap.InvokeOnceAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ public void Dispose_DisposesInnerStream()
// LambdaResponseStreamFactory tests
// ─────────────────────────────────────────────────────────────────────────────

[Collection("ResponseStreamFactory")]
[Collection("RuntimeSupportStateCheck")]
public class LambdaResponseStreamFactoryTests : IDisposable
{

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

namespace Amazon.Lambda.RuntimeSupport.UnitTests
{
[Collection("ResponseStreamFactory")]
[Collection("RuntimeSupportStateCheck")]
public class ResponseStreamFactoryTests : IDisposable
{
private const long MaxResponseSize = 20 * 1024 * 1024;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@

namespace Amazon.Lambda.RuntimeSupport.UnitTests
{
[CollectionDefinition("ResponseStreamFactory")]
public class ResponseStreamFactoryCollection { }
[CollectionDefinition("RuntimeSupportStateCheck")]
public class RuntimeSupportStateCheckCollection { }

/// <summary>
/// End-to-end integration tests for the true-streaming architecture.
/// These tests exercise the full pipeline: LambdaBootstrap → ResponseStreamFactory →
/// ResponseStream → captured HTTP output stream.
/// </summary>
[Collection("ResponseStreamFactory")]
[Collection("RuntimeSupportStateCheck")]
public class StreamingE2EWithMoq : IDisposable
{
public void Dispose()
Expand Down Expand Up @@ -162,7 +162,7 @@ public async Task Streaming_AllDataTransmitted_ContentRoundTrip()
return new InvocationResponse(Stream.Null, false);
};

using var bootstrap = new LambdaBootstrap(handler, null);
using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables());
bootstrap.Client = client;
await bootstrap.InvokeOnceAsync();

Expand All @@ -189,7 +189,7 @@ public async Task Streaming_StreamFinalized_BytesWrittenMatchesPayload()
return new InvocationResponse(Stream.Null, false);
};

using var bootstrap = new LambdaBootstrap(handler, null);
using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables());
bootstrap.Client = client;
await bootstrap.InvokeOnceAsync();

Expand All @@ -213,7 +213,7 @@ public async Task Buffered_HandlerDoesNotCallCreateStream_UsesSendResponsePath()
return new InvocationResponse(new MemoryStream(responseBody));
};

using var bootstrap = new LambdaBootstrap(handler, null);
using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables());
bootstrap.Client = client;
await bootstrap.InvokeOnceAsync();

Expand All @@ -237,7 +237,7 @@ public async Task Buffered_ResponseBodyTransmittedCorrectly()
return new InvocationResponse(new MemoryStream(responseBody));
};

using var bootstrap = new LambdaBootstrap(handler, null);
using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables());
bootstrap.Client = client;
await bootstrap.InvokeOnceAsync();

Expand All @@ -248,51 +248,6 @@ public async Task Buffered_ResponseBodyTransmittedCorrectly()
Assert.Equal(responseBody, received.ToArray());
}

/// <summary>
/// End-to-end: midstream error sets error state on ResponseStream with exception details.
/// In production, RawStreamingHttpClient reads this state and writes trailing headers.
/// </summary>
[Fact]
public async Task MidstreamError_SetsErrorStateWithExceptionDetails()
{
var client = CreateClient();
const string errorMessage = "something went wrong mid-stream";

// Signal so the handler waits until the streaming pipeline is fully
// established (WaitForCompletionAsync is actively listening) before throwing.
var streamingReady = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
client.OnStreamingReady = () => streamingReady.TrySetResult(true);

LambdaBootstrapHandler handler = async (invocation) =>
{
var stream = ResponseStreamFactory.CreateStream(Array.Empty<byte>());
await stream.WriteAsync(Encoding.UTF8.GetBytes("some data"));
// Wait until StartStreamingResponseAsync has reached WaitForCompletionAsync
// so the completion signal will be observed when ReportError fires.
await streamingReady.Task;
throw new InvalidOperationException(errorMessage);
};

using var bootstrap = new LambdaBootstrap(handler, null);
bootstrap.Client = client;
await bootstrap.InvokeOnceAsync();

Assert.True(client.StartStreamingCalled);
Assert.NotNull(client.LastResponseStream);
Assert.True(client.LastResponseStream.HasError);
Assert.NotNull(client.LastResponseStream.ReportedError);
Assert.IsType<InvalidOperationException>(client.LastResponseStream.ReportedError);
Assert.Equal(errorMessage, client.LastResponseStream.ReportedError.Message);

// Verify the handler's data was still captured before the error.
// Read directly from the output stream that was provided to the ResponseStream,
// which avoids any timing dependency on when CapturedHttpBytes is assigned
// relative to the SendTask completion.
Assert.NotNull(client.CapturedOutputStream);
var output = Encoding.UTF8.GetString(client.CapturedOutputStream.ToArray());
Assert.Contains("some data", output);
}

/// <summary>
/// Multi-concurrency: concurrent invocations use AsyncLocal for state isolation.
/// Each invocation independently uses streaming or buffered mode without interference.
Expand Down Expand Up @@ -457,7 +412,7 @@ public async Task BackwardCompat_ExistingHandlerSignature_WorksUnchanged()
return new InvocationResponse(new MemoryStream(Encoding.UTF8.GetBytes("classic response")));
};

using var bootstrap = new LambdaBootstrap(handler, null);
using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables());
bootstrap.Client = client;
await bootstrap.InvokeOnceAsync();

Expand All @@ -481,7 +436,7 @@ public async Task BackwardCompat_BufferedResponse_NoRegression()
return new InvocationResponse(new MemoryStream(expected));
};

using var bootstrap = new LambdaBootstrap(handler, null);
using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables());
bootstrap.Client = client;
await bootstrap.InvokeOnceAsync();

Expand All @@ -506,7 +461,7 @@ public async Task BackwardCompat_NullOutputStream_HandledGracefully()
return new InvocationResponse(Stream.Null, false);
};

using var bootstrap = new LambdaBootstrap(handler, null);
using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables());
bootstrap.Client = client;

// Should not throw
Expand All @@ -529,7 +484,7 @@ public async Task BackwardCompat_HandlerThrows_StandardErrorReportingUsed()
throw new Exception("classic handler error");
};

using var bootstrap = new LambdaBootstrap(handler, null);
using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables());
bootstrap.Client = client;
await bootstrap.InvokeOnceAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public TestRuntimeApiClient(IEnvironmentVariables environmentVariables, Dictiona

public string LastTraceId { get; private set; }
public byte[] FunctionInput { get; set; }
public Stream LastOutputStream { get; private set; }
public Stream LastOutputStream { get; internal set; }
public Exception LastRecordedException { get; private set; }

public void VerifyOutput(string expectedOutput)
Expand Down
Loading
Loading