Skip to content

Handle error conditions from Lambda function in Runtime API #1953

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

Merged
4 commits merged into from
Jan 30, 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
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,48 @@ public static APIGatewayProxyResponse ToApiGatewayProxyResponse(this InvokeRespo
}
catch
{
if (emulatorMode == ApiGatewayEmulatorMode.Rest)
return ToApiGatewayErrorResponse(emulatorMode);
}
}

/// <summary>
/// Creates an API Gateway error response based on the emulator mode.
/// </summary>
/// <param name="emulatorMode">The API Gateway emulator mode (Rest or Http).</param>
/// <returns>An APIGatewayProxyResponse object representing the error response.</returns>
/// <remarks>
/// This method generates different error responses based on the API Gateway emulator mode:
/// - For Rest mode: Returns a response with StatusCode 502 and a generic error message.
/// - For Http mode: Returns a response with StatusCode 500 and a generic error message.
/// Both responses include a Content-Type header set to application/json.
/// </remarks>
public static APIGatewayProxyResponse ToApiGatewayErrorResponse(ApiGatewayEmulatorMode emulatorMode)
{
if (emulatorMode == ApiGatewayEmulatorMode.Rest)
{
return new APIGatewayProxyResponse
{
return new APIGatewayProxyResponse
{
StatusCode = 502,
Body = "{\"message\":\"Internal server error\"}",
Headers = new Dictionary<string, string>
StatusCode = 502,
Body = "{\"message\":\"Internal server error\"}",
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
},
IsBase64Encoded = false
};
}
else
IsBase64Encoded = false
};
}
else
{
return new APIGatewayProxyResponse
{
return new APIGatewayProxyResponse
{
StatusCode = 500,
Body = "{\"message\":\"Internal Server Error\"}",
Headers = new Dictionary<string, string>
StatusCode = 500,
Body = "{\"message\":\"Internal Server Error\"}",
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
},
IsBase64Encoded = false
};
}
IsBase64Encoded = false
};
}
}

Expand Down Expand Up @@ -137,16 +153,7 @@ private static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2Response(string respo
catch
{
// If deserialization fails, return Internal Server Error
return new APIGatewayHttpApiV2ProxyResponse
Copy link
Author

Choose a reason for hiding this comment

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

i just put this in its own function so i can re-use it

{
StatusCode = 500,
Body = "{\"message\":\"Internal Server Error\"}",
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
},
IsBase64Encoded = false
};
return ToHttpApiV2ErrorResponse();
}
}

Expand Down Expand Up @@ -177,4 +184,29 @@ private static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2Response(string respo
};
}

/// <summary>
/// Creates a standard HTTP API v2 error response.
/// </summary>
/// <returns>An APIGatewayHttpApiV2ProxyResponse object representing the error response.</returns>
/// <remarks>
/// This method generates a standard error response for HTTP API v2:
/// - StatusCode is set to 500 (Internal Server Error).
/// - Body contains a JSON string with a generic error message.
/// - Headers include a Content-Type set to application/json.
/// - IsBase64Encoded is set to false.
/// </remarks>
public static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2ErrorResponse()
{
return new APIGatewayHttpApiV2ProxyResponse
{
StatusCode = 500,
Body = "{\"message\":\"Internal Server Error\"}",
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
},
IsBase64Encoded = false
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -99,24 +99,35 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
using var lambdaClient = CreateLambdaServiceClient(routeConfig, settings);
var response = await lambdaClient.InvokeAsync(invokeRequest);

if (response.FunctionError != null)
if (response.FunctionError == null) // response is successful
{
// TODO: Mimic API Gateway's behavior when Lambda function has an exception during invocation.
context.Response.StatusCode = 500;
return;
}

// Convert API Gateway response object returned from Lambda to ASP.NET Core response.
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
{
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 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))
{
var lambdaResponse = InvokeResponseExtensions.ToHttpApiV2ErrorResponse();
Copy link
Author

Choose a reason for hiding this comment

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

I kept these function (ToErrorResponse) inside the extensions class because I wasn't sure else where to put them, even though they really arent being used as extensions. I figured it's fine for now but if anyone has better ideas let me know.

await lambdaResponse.ToHttpResponseAsync(context);
}
else
{
var lambdaResponse = InvokeResponseExtensions.ToApiGatewayErrorResponse(settings.ApiGatewayEmulatorMode.Value);
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
}
}

});

var runTask = app.RunAsync(cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Amazon.Lambda.TestTool.UnitTests")]
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Collections.Concurrent;
using System.Text;
using Amazon.Lambda.TestTool.Models;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -15,7 +14,7 @@ public class LambdaRuntimeApi

private readonly IRuntimeApiDataStoreManager _runtimeApiDataStoreManager;

private LambdaRuntimeApi(WebApplication app)
internal LambdaRuntimeApi(WebApplication app)
{
_runtimeApiDataStoreManager = app.Services.GetRequiredService<IRuntimeApiDataStoreManager>();

Expand Down Expand Up @@ -63,16 +62,31 @@ public async Task PostEvent(HttpContext ctx, string functionName)
if (isRequestResponseMode)
{
evnt.WaitForCompletion();
var result = Results.Ok(evnt.Response);
ctx.Response.StatusCode = 200;

if (!string.IsNullOrEmpty(evnt.Response))
if (evnt.EventStatus == EventContainer.Status.Success)
{
var responseData = Encoding.UTF8.GetBytes(evnt.Response);
ctx.Response.Headers.ContentType = "application/json";
ctx.Response.Headers.ContentLength = responseData.Length;

await ctx.Response.Body.WriteAsync(responseData);
var result = Results.Ok(evnt.Response);
ctx.Response.StatusCode = 200;

if (!string.IsNullOrEmpty(evnt.Response))
{
var responseData = Encoding.UTF8.GetBytes(evnt.Response);
ctx.Response.Headers.ContentType = "application/json";
ctx.Response.Headers.ContentLength = responseData.Length;
await ctx.Response.Body.WriteAsync(responseData);
}
}
else
{
ctx.Response.StatusCode = 200;
ctx.Response.Headers["X-Amz-Function-Error"] = evnt.ErrorType;
if (!string.IsNullOrEmpty(evnt.ErrorResponse))
{
var errorData = Encoding.UTF8.GetBytes(evnt.ErrorResponse);
ctx.Response.Headers.ContentType = "application/json";
ctx.Response.Headers.ContentLength = errorData.Length;
await ctx.Response.Body.WriteAsync(errorData);
}
}
evnt.Dispose();
}
Expand Down
Loading
Loading