Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 6MB request and response size check #1990

Merged
merged 20 commits into from
Feb 26, 2025
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
11 changes: 11 additions & 0 deletions .autover/changes/e65c21d6-7608-481f-8135-198a7ab8ad54.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.TestTool",
"Type": "Patch",
"ChangelogMessages": [
"Add 6MB request and response size validation."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,26 @@ else
<label for="sample-requests">Example Requests</label>
</div>
<div class="mt-1 flex-grow-1 flex-fill">
<label class="form-label" for="function-payload">Function Input</label>
<StandaloneCodeEditor Id="function-payload" @ref="_editor" ConstructionOptions="EditorConstructionOptions" CssClass="rounded-4 overflow-hidden border"/>
<div class="d-flex justify-content-start align-items-center">
<label class="form-label mb-0" for="function-payload">Function Input</label>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="text-danger ms-auto">
<i class="bi bi-exclamation-triangle-fill"></i>
</div>
}
</div>
<StandaloneCodeEditor
Id="function-payload"
@ref="_editor"
ConstructionOptions="EditorConstructionOptions"
CssClass="@($"rounded-4 overflow-hidden border {(!string.IsNullOrEmpty(_errorMessage) ? "border-danger border-2" : "")}")"/>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="text-danger">
@_errorMessage
</div>
}
</div>
</div>
<div class="col-lg-6 d-flex flex-column">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using Amazon.Lambda.Model;
using Microsoft.AspNetCore.Components;
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Models;
Expand All @@ -9,6 +10,7 @@
using BlazorMonaco.Editor;
using Microsoft.JSInterop;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options;

namespace Amazon.Lambda.TestTool.Components.Pages;

Expand All @@ -21,6 +23,8 @@ public partial class Home : ComponentBase, IDisposable
[Inject] public required IDirectoryManager DirectoryManager { get; set; }
[Inject] public required IThemeService ThemeService { get; set; }
[Inject] public required IJSRuntime JsRuntime { get; set; }
[Inject] public required ILambdaClient LambdaClient { get; set; }
[Inject] public IOptions<LambdaOptions> LambdaOptions { get; set; }

private StandaloneCodeEditor? _editor;
private StandaloneCodeEditor? _activeEditor;
Expand All @@ -33,6 +37,8 @@ public partial class Home : ComponentBase, IDisposable

private const string NoSampleSelectedId = "void-select-request";

private string _errorMessage = string.Empty;

private IDictionary<string, IList<LambdaRequest>> SampleRequests { get; set; } = new Dictionary<string, IList<LambdaRequest>>();

private IRuntimeApiDataStore? DataStore { get; set; }
Expand Down Expand Up @@ -184,9 +190,12 @@ async Task OnAddEventClick()
DataStore is null)
return;
var editorValue = await _editor.GetValue();
DataStore.QueueEvent(editorValue, false);
await _editor.SetValue(string.Empty);
SelectedSampleRequestName = NoSampleSelectedId;
var success = await InvokeLambdaFunction(editorValue);
if (success)
{
await _editor.SetValue(string.Empty);
SelectedSampleRequestName = NoSampleSelectedId;
}
StateHasChanged();
}

Expand All @@ -202,7 +211,7 @@ void OnClearExecuted()
StateHasChanged();
}

void OnRequeue(string awsRequestId)
async Task OnRequeue(string awsRequestId)
{
if (DataStore is null)
return;
Expand All @@ -218,8 +227,7 @@ void OnRequeue(string awsRequestId)

if (evnt == null)
return;

DataStore.QueueEvent(evnt.EventJson, false);
await InvokeLambdaFunction(evnt.EventJson);
StateHasChanged();
}

Expand Down Expand Up @@ -326,4 +334,32 @@ private StandaloneEditorConstructionOptions ActiveErrorEditorConstructionOptions
}
};
}

private async Task<bool> InvokeLambdaFunction(string payload)
{
var invokeRequest = new InvokeRequest
{
FunctionName = SelectedFunctionName,
Payload = payload,
InvocationType = InvocationType.Event
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the invocation type "Event" and not "RequestResponse"?

Copy link
Contributor Author

@GarrettBeatty GarrettBeatty Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the original implementation was also set to Event type (When invocated as DataStore.QueueEvent(editorValue, false); , the false was for the isRequestResponse boolean, so kept it the same

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event is what we want here because we just want to queue the event. We don't want to block the UI waiting for the response.

};

try
{
await LambdaClient.InvokeAsync(invokeRequest, LambdaOptions.Value.Endpoint);
_errorMessage = string.Empty;
return true;
}
catch (AmazonLambdaException e)
{
Logger.LogInformation(e.Message);

// lambda client automatically adds some extra verbiage: "The service returned an error with Error Code xxxx and HTTP Body: <bodyhere>".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: log the entire error using the logger

// removing the extra verbiage to make the error message smaller and look better on the ui.
_errorMessage = e.Message.Contains("HTTP Body: ")
? e.Message.Split("HTTP Body: ")[1]
: e.Message;
}
return false;
}
}
16 changes: 16 additions & 0 deletions Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Exceptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

namespace Amazon.Lambda.TestTool;

/// <summary>
/// Contains constant string values for AWS Lambda exception types.
/// </summary>
public class Exceptions
{
/// <summary>
/// Exception thrown when the request payload size exceeds AWS Lambda's limits.
/// This occurs when the request payload is larger than 6 MB for synchronous invocations.
/// </summary>
public const string RequestEntityTooLargeException = "RequestEntityTooLargeException";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Amazon.Lambda.Model;
using Amazon.Lambda.TestTool.Models;

namespace Amazon.Lambda.TestTool.Extensions;

/// <summary>
/// Provides extension methods for converting Lambda InvokeResponse to API Gateway response types.
/// </summary>
Expand Down Expand Up @@ -83,6 +85,52 @@ public static APIGatewayProxyResponse ToApiGatewayErrorResponse(ApiGatewayEmulat
}
}

/// <summary>
/// Creates a standard API Gateway response for a "Request Entity Too Large" (413) error.
/// Not compatible with HTTP V2 API Gateway mode.
/// </summary>
/// <param name="emulatorMode">The API Gateway emulator mode (Rest or HttpV1 only).</param>
/// <returns>An APIGatewayProxyResponse configured with:
/// - Status code 413
/// - JSON error message ("Request Too Long" for REST, "Request Entity Too Large" for HTTP V1)
/// - Content-Type header set to application/json
/// </returns>
/// <exception cref="InvalidOperationException">Thrown when emulatorMode is HttpV2 or invalid value</exception>
/// <remarks>
/// This method only supports REST and HTTP V1 API Gateway modes. For HTTP V2,
/// use <seealso cref="ToHttpApiV2RequestTooLargeResponse"/>.
/// </remarks>
public static APIGatewayProxyResponse ToHttpApiRequestTooLargeResponse(ApiGatewayEmulatorMode emulatorMode)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, does this not apply to HttpV2? If not, maybe add a note to the method docs.

Copy link
Contributor Author

@GarrettBeatty GarrettBeatty Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theres a separate function for httpv2, ill update the docs

{
if (emulatorMode == ApiGatewayEmulatorMode.Rest)
{
return new APIGatewayProxyResponse
{
StatusCode = 413,
Body = "{\"message\":\"Request Too Long\"}",
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
},
IsBase64Encoded = false
};
}
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1)
{
return new APIGatewayProxyResponse
{
StatusCode = 413,
Body = "{\"message\":\"Request Entity Too Large\"}",
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
},
IsBase64Encoded = false
};
}
throw new ArgumentException($"Unsupported API Gateway emulator mode: {emulatorMode}. Only Rest and HttpV1 modes are supported.");
}

/// <summary>
/// Converts an Amazon Lambda InvokeResponse to an APIGatewayHttpApiV2ProxyResponse.
/// </summary>
Expand Down Expand Up @@ -209,4 +257,25 @@ public static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2ErrorResponse()
};
}

/// <summary>
/// Creates a standard HTTP API v2 response for a "Request Entity Too Large" (413) error.
/// </summary>
/// <returns>An APIGatewayHttpApiV2ProxyResponse configured with:
/// - Status code 413
/// - JSON error message
/// - Content-Type header set to application/json
/// </returns>
public static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2RequestTooLargeResponse()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same comment about naming

{
return new APIGatewayHttpApiV2ProxyResponse
{
StatusCode = 413,
Body = "{\"message\":\"Request Entity Too Large\"}",
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
},
IsBase64Encoded = false
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

namespace Amazon.Lambda.TestTool.Models;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasnt sure where to put this in models folder or in LambdaClient file/folder


/// <summary>
/// Configuration options for invoking lambda functions.
/// </summary>
public class LambdaOptions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docs for class and members

{
/// <summary>
/// Gets or sets the endpoint URL for Lambda function invocations.
/// </summary>
/// <value>
/// A string containing the endpoint URL. Defaults to an empty string.
/// </value>
public string Endpoint { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
Utils.ConfigureWebApplicationBuilder(builder);

builder.Services.AddApiGatewayEmulatorServices();
builder.Services.AddSingleton<ILambdaClient, LambdaClient>();

var serviceUrl = $"http://{settings.LambdaEmulatorHost}:{settings.ApiGatewayEmulatorPort}";
builder.WebHost.UseUrls(serviceUrl);
Expand All @@ -68,7 +69,7 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
app.Logger.LogInformation("The API Gateway Emulator is available at: {ServiceUrl}", serviceUrl);
});

app.Map("/{**catchAll}", async (HttpContext context, IApiGatewayRouteConfigService routeConfigService) =>
app.Map("/{**catchAll}", async (HttpContext context, IApiGatewayRouteConfigService routeConfigService, ILambdaClient lambdaClient) =>
{
var routeConfig = routeConfigService.GetRouteConfig(context.Request.Method, context.Request.Path);
if (routeConfig == null)
Expand Down Expand Up @@ -101,38 +102,56 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
PayloadStream = lambdaRequestStream
};

using var lambdaClient = CreateLambdaServiceClient(routeConfig, settings);
var response = await lambdaClient.InvokeAsync(invokeRequest);

if (response.FunctionError == null) // response is successful
try
{
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
var endpoint = routeConfig.Endpoint ?? $"http://{settings.LambdaEmulatorHost}:{settings.LambdaEmulatorPort}";
var response = await lambdaClient.InvokeAsync(invokeRequest, endpoint);

if (response.FunctionError == null) // response is successful
{
var lambdaResponse = response.ToApiGatewayHttpApiV2ProxyResponse();
await lambdaResponse.ToHttpResponseAsync(context);
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
{
var lambdaResponse = response.ToApiGatewayHttpApiV2ProxyResponse();
await lambdaResponse.ToHttpResponseAsync(context);
}
else
{
var lambdaResponse = response.ToApiGatewayProxyResponse(settings.ApiGatewayEmulatorMode.Value);
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
}
}
else
{
var lambdaResponse = response.ToApiGatewayProxyResponse(settings.ApiGatewayEmulatorMode.Value);
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
// For errors that happen within the function they still come back as 200 status code (they dont throw exception) but have FunctionError populated.
// Api gateway just displays them as an internal server error, so we convert them to the correct error response here.
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
{
var lambdaResponse = InvokeResponseExtensions.ToHttpApiV2ErrorResponse();
await lambdaResponse.ToHttpResponseAsync(context);
}
else
{
var lambdaResponse = InvokeResponseExtensions.ToApiGatewayErrorResponse(settings.ApiGatewayEmulatorMode.Value);
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
}
}
}
else
catch (AmazonLambdaException e)
{
// For function errors, api gateway just displays them as an internal server error, so we convert them to the correct error response here.

if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
if (e.ErrorCode == Exceptions.RequestEntityTooLargeException)
{
var lambdaResponse = InvokeResponseExtensions.ToHttpApiV2ErrorResponse();
await lambdaResponse.ToHttpResponseAsync(context);
}
else
{
var lambdaResponse = InvokeResponseExtensions.ToApiGatewayErrorResponse(settings.ApiGatewayEmulatorMode.Value);
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
{
var lambdaResponse = InvokeResponseExtensions.ToHttpApiV2RequestTooLargeResponse();
await lambdaResponse.ToHttpResponseAsync(context);
}
else
{
var lambdaResponse = InvokeResponseExtensions.ToHttpApiRequestTooLargeResponse(settings.ApiGatewayEmulatorMode.Value);
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
}
}
}

});

var runTask = app.RunAsync(cancellationToken);
Expand All @@ -144,30 +163,4 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
ServiceUrl = serviceUrl
};
}

/// <summary>
/// Creates an Amazon Lambda service client with the specified configuration.
/// </summary>
/// <param name="routeConfig">The API Gateway route configuration containing the endpoint information.
/// If the endpoint is specified in routeConfig, it will be used as the service URL.</param>
/// <param name="settings">The run command settings containing host and port information.
/// If routeConfig endpoint is null, the service URL will be constructed using settings.Host and settings.Port.</param>
/// <returns>An instance of IAmazonLambda configured with the specified endpoint and credentials.</returns>
/// <remarks>
/// The function uses hard-coded AWS credentials ("accessKey", "secretKey") for authentication since they are not actually being used.
/// The service URL is determined by either:
/// - Using routeConfig.Endpoint if it's not null
/// - Combining settings.Host and settings.Port if routeConfig.Endpoint is null
/// </remarks>
private static IAmazonLambda CreateLambdaServiceClient(ApiGatewayRouteConfig routeConfig, RunCommandSettings settings)
{
var endpoint = routeConfig.Endpoint ?? $"http://{settings.LambdaEmulatorHost}:{settings.LambdaEmulatorPort}";

var lambdaConfig = new AmazonLambdaConfig
{
ServiceURL = endpoint
};

return new AmazonLambdaClient(new Amazon.Runtime.BasicAWSCredentials("accessKey", "secretKey"), lambdaConfig);
}
}
Loading
Loading