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 ApiGatewayHttpApiV2ProxyRequestTranslator and ApiGatewayProxyRequestTranslator #1901

Open
wants to merge 10 commits into
base: asmarp/api-gateway-emulator-skeleton
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageReference Include="Blazored.Modal" Version="7.3.1" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
using Amazon.Lambda.TestTool.Models;

gcbeattyAWS marked this conversation as resolved.
Show resolved Hide resolved
namespace Amazon.Lambda.TestTool.Extensions;

using Amazon.Lambda.TestTool.Models;

namespace Amazon.Lambda.TestTool.Extensions;

/// <summary>
/// A class that contains extension methods for the <see cref="Exception"/> class.
/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// True if the <paramref name="e"/> inherits from
/// <see cref="TestToolException"/>.
/// </summary>
public static bool IsExpectedException(this Exception e) =>
e is TestToolException;

/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// True if the <paramref name="e"/> inherits from
/// <see cref="TestToolException"/>.
/// </summary>
public static bool IsExpectedException(this Exception e) =>
e is TestToolException;

/// <summary>
/// Prints an exception in a user-friendly way.
/// </summary>
public static string PrettyPrint(this Exception? e)
{
if (null == e)
return string.Empty;

return $"{Environment.NewLine}{e.Message}" +
$"{Environment.NewLine}{e.StackTrace}" +
$"{PrettyPrint(e.InnerException)}";
}
/// </summary>
public static string PrettyPrint(this Exception? e)
{
if (null == e)
return string.Empty;

return $"{Environment.NewLine}{e.Message}" +
$"{Environment.NewLine}{e.StackTrace}" +
$"{PrettyPrint(e.InnerException)}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace Amazon.Lambda.TestTool.Extensions;

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Utilities;
using System.Text;
using System.Web;
using static Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest;

/// <summary>
/// Provides extension methods to translate an <see cref="HttpContext"/> to different types of API Gateway requests.
/// </summary>
public static class HttpContextExtensions
{

/// <summary>
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayHttpApiV2ProxyRequest"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
/// <returns>An <see cref="APIGatewayHttpApiV2ProxyRequest"/> object representing the translated request.</returns>
public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
this HttpContext context,
ApiGatewayRouteConfig apiGatewayRouteConfig)
{
var request = context.Request;

var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
gcbeattyAWS marked this conversation as resolved.
Show resolved Hide resolved

var (headers, _) = HttpRequestUtility.ExtractHeaders(request.Headers);
var (queryStringParameters, _) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);

var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest
{
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
RawPath = request.Path,
RawQueryString = request.QueryString.Value,
Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray(),
Headers = headers,
QueryStringParameters = queryStringParameters,
PathParameters = pathParameters ?? new Dictionary<string, string>(),
Body = HttpRequestUtility.ReadRequestBody(request),
Copy link
Member

Choose a reason for hiding this comment

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

Can you take note that when we get these ToAPIGatewayXXX() methods hooked up to the main code path that is asking for the APIGatewayHttpApiV2ProxyRequest. When we convert the APIGatewayHttpApiV2ProxyRequest to the MemoryStream to Lambda we should check the size of the stream is no more then 6MB. If so throw back the same error that users would get from API Gateway.

Copy link
Author

Choose a reason for hiding this comment

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

yeah i will connect with phil on where he wants to do this logic at

IsBase64Encoded = false,
RequestContext = new ProxyRequestContext
{
Http = new HttpDescription
{
Method = request.Method,
Path = request.Path,
Protocol = request.Protocol
},
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}"
},
Version = "2.0"
};

if (HttpRequestUtility.IsBinaryContent(request.ContentType))
{
httpApiV2ProxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(httpApiV2ProxyRequest.Body));
httpApiV2ProxyRequest.IsBase64Encoded = true;
}

return httpApiV2ProxyRequest;
}

/// <summary>
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayProxyRequest"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
/// <returns>An <see cref="APIGatewayProxyRequest"/> object representing the translated request.</returns>
public static APIGatewayProxyRequest ToApiGatewayRequest(
this HttpContext context,
ApiGatewayRouteConfig apiGatewayRouteConfig)
{
var request = context.Request;

var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);

var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);

var proxyRequest = new APIGatewayProxyRequest
{
Resource = apiGatewayRouteConfig.Path,
Path = HttpUtility.UrlEncode(request.Path),
HttpMethod = request.Method,
Headers = headers,
MultiValueHeaders = multiValueHeaders,
QueryStringParameters = queryStringParameters,
MultiValueQueryStringParameters = multiValueQueryStringParameters,
PathParameters = pathParameters ?? new Dictionary<string, string>(),
Copy link
Member

Choose a reason for hiding this comment

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

Why are we initializing this to an empty collection if we don't have any. Is this what API Gateway does?

Copy link
Author

Choose a reason for hiding this comment

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

good catch. i just did a test
Invoke-WebRequest -Uri https://myapi-gateway-url.amazonaws.com/testfunction -Method GET
where it echos the api gateway request object and i see the value is null when there are no path parameters.

pathParameters: null,
  stageVariables: null,
  body: null,
  isBase64Encoded: false
}

i will update the code

Copy link
Author

Choose a reason for hiding this comment

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

ill also double check the query string and header default values

Copy link
Author

@gcbeattyAWS gcbeattyAWS Dec 12, 2024

Choose a reason for hiding this comment

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

ive updated the logic to handle this now

Body = HttpRequestUtility.ReadRequestBody(request),
IsBase64Encoded = false
};

if (HttpRequestUtility.IsBinaryContent(request.ContentType))
{
proxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(proxyRequest.Body));
proxyRequest.IsBase64Encoded = true;
}

return proxyRequest;
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Services.IO;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Amazon.Lambda.TestTool.Extensions;

/// <summary>
/// A class that contains extension methods for the <see cref="IServiceCollection"/> interface.
/// </summary>
public static class ServiceCollectionExtensions
{
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Services.IO;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Amazon.Lambda.TestTool.Extensions;

/// <summary>
/// A class that contains extension methods for the <see cref="IServiceCollection"/> interface.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds a set of services for the .NET CLI portion of this application.
/// </summary>
public static void AddCustomServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
/// </summary>
public static void AddCustomServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
}

/// <summary>
/// Adds a set of services for the API Gateway emulator portion of this application.
/// </summary>
public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
}
/// </summary>
public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ public class ApiGatewayRouteConfig
/// The name of the Lambda function
/// </summary>
public required string LambdaResourceName { get; set; }

/// <summary>
/// The endpoint of the local Lambda Runtime API
/// </summary>
public string? Endpoint { get; set; }

/// <summary>
/// The HTTP Method for the API Gateway endpoint
/// </summary>
public required string HttpMethod { get; set; }

/// <summary>
/// The API Gateway HTTP Path of the Lambda function
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,78 +1,78 @@
using Amazon.Lambda.TestTool.Commands.Settings;
using Amazon.Lambda.TestTool.Extensions;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services;

namespace Amazon.Lambda.TestTool.Processes;

/// <summary>
/// A process that runs the API Gatewat emulator.
/// </summary>
public class ApiGatewayEmulatorProcess
{
/// <summary>
/// The service provider that will contain all the registered services.
/// </summary>
using Amazon.Lambda.TestTool.Commands.Settings;
using Amazon.Lambda.TestTool.Extensions;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services;
namespace Amazon.Lambda.TestTool.Processes;
/// <summary>
/// A process that runs the API Gatewat emulator.
/// </summary>
public class ApiGatewayEmulatorProcess
{
/// <summary>
/// The service provider that will contain all the registered services.
/// </summary>
public required IServiceProvider Services { get; init; }

/// <summary>
/// The API Gatewat emulator task that was started.
/// </summary>
/// <summary>
/// The API Gatewat emulator task that was started.
/// </summary>
public required Task RunningTask { get; init; }

/// <summary>
/// The endpoint of the API Gatewat emulator.
/// </summary>
/// </summary>
public required string ServiceUrl { get; init; }

/// <summary>
/// Creates the Web API and runs it in the background.
/// </summary>
public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, CancellationToken cancellationToken = default)
{
var builder = WebApplication.CreateBuilder();

builder.Services.AddApiGatewayEmulatorServices();

var serviceUrl = $"http://{settings.Host}:{settings.ApiGatewayEmulatorPort}";
builder.WebHost.UseUrls(serviceUrl);
/// </summary>
public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, CancellationToken cancellationToken = default)
{
var builder = WebApplication.CreateBuilder();
builder.Services.AddApiGatewayEmulatorServices();
var serviceUrl = $"http://{settings.Host}:{settings.ApiGatewayEmulatorPort}";
builder.WebHost.UseUrls(serviceUrl);
builder.WebHost.SuppressStatusMessages(true);

builder.Services.AddHealthChecks();

var app = builder.Build();

var app = builder.Build();
app.UseHttpsRedirection();

app.MapHealthChecks("/health");

app.Map("/{**catchAll}", (HttpContext context, IApiGatewayRouteConfigService routeConfigService) =>
{
var routeConfig = routeConfigService.GetRouteConfig(context.Request.Method, context.Request.Path);
if (routeConfig == null)
{
return Results.NotFound("Route not found");
}

if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
{
// TODO: Translate to APIGatewayHttpApiV2ProxyRequest
}
else
{
// TODO: Translate to APIGatewayProxyRequest
}

return Results.Ok();
});

var runTask = app.RunAsync(cancellationToken);

return new ApiGatewayEmulatorProcess
{
Services = app.Services,
RunningTask = runTask,
ServiceUrl = serviceUrl
};
}
app.MapHealthChecks("/health");
app.Map("/{**catchAll}", (HttpContext context, IApiGatewayRouteConfigService routeConfigService) =>
{
var routeConfig = routeConfigService.GetRouteConfig(context.Request.Method, context.Request.Path);
if (routeConfig == null)
{
return Results.NotFound("Route not found");
}
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
{
// TODO: Translate to APIGatewayHttpApiV2ProxyRequest
}
else
{
// TODO: Translate to APIGatewayProxyRequest
}
return Results.Ok();
});
var runTask = app.RunAsync(cancellationToken);
return new ApiGatewayEmulatorProcess
{
Services = app.Services,
RunningTask = runTask,
ServiceUrl = serviceUrl
};
}
}
Loading
Loading