Skip to content

Commit

Permalink
Add 6MB request and response size check (#1990)
Browse files Browse the repository at this point in the history
  • Loading branch information
GarrettBeatty authored Feb 26, 2025
1 parent 4e50b80 commit 700568a
Show file tree
Hide file tree
Showing 17 changed files with 618 additions and 62 deletions.
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
};

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>".
// 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)
{
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()
{
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;

/// <summary>
/// Configuration options for invoking lambda functions.
/// </summary>
public class LambdaOptions
{
/// <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

0 comments on commit 700568a

Please sign in to comment.