Skip to content

Commit 6986f04

Browse files
Handle error conditions from Lambda function in Runtime API (#1953)
1 parent 6eb296f commit 6986f04

File tree

6 files changed

+452
-70
lines changed

6 files changed

+452
-70
lines changed

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/InvokeResponseExtensions.cs

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,32 +38,48 @@ public static APIGatewayProxyResponse ToApiGatewayProxyResponse(this InvokeRespo
3838
}
3939
catch
4040
{
41-
if (emulatorMode == ApiGatewayEmulatorMode.Rest)
41+
return ToApiGatewayErrorResponse(emulatorMode);
42+
}
43+
}
44+
45+
/// <summary>
46+
/// Creates an API Gateway error response based on the emulator mode.
47+
/// </summary>
48+
/// <param name="emulatorMode">The API Gateway emulator mode (Rest or Http).</param>
49+
/// <returns>An APIGatewayProxyResponse object representing the error response.</returns>
50+
/// <remarks>
51+
/// This method generates different error responses based on the API Gateway emulator mode:
52+
/// - For Rest mode: Returns a response with StatusCode 502 and a generic error message.
53+
/// - For Http mode: Returns a response with StatusCode 500 and a generic error message.
54+
/// Both responses include a Content-Type header set to application/json.
55+
/// </remarks>
56+
public static APIGatewayProxyResponse ToApiGatewayErrorResponse(ApiGatewayEmulatorMode emulatorMode)
57+
{
58+
if (emulatorMode == ApiGatewayEmulatorMode.Rest)
59+
{
60+
return new APIGatewayProxyResponse
4261
{
43-
return new APIGatewayProxyResponse
44-
{
45-
StatusCode = 502,
46-
Body = "{\"message\":\"Internal server error\"}",
47-
Headers = new Dictionary<string, string>
62+
StatusCode = 502,
63+
Body = "{\"message\":\"Internal server error\"}",
64+
Headers = new Dictionary<string, string>
4865
{
4966
{ "Content-Type", "application/json" }
5067
},
51-
IsBase64Encoded = false
52-
};
53-
}
54-
else
68+
IsBase64Encoded = false
69+
};
70+
}
71+
else
72+
{
73+
return new APIGatewayProxyResponse
5574
{
56-
return new APIGatewayProxyResponse
57-
{
58-
StatusCode = 500,
59-
Body = "{\"message\":\"Internal Server Error\"}",
60-
Headers = new Dictionary<string, string>
75+
StatusCode = 500,
76+
Body = "{\"message\":\"Internal Server Error\"}",
77+
Headers = new Dictionary<string, string>
6178
{
6279
{ "Content-Type", "application/json" }
6380
},
64-
IsBase64Encoded = false
65-
};
66-
}
81+
IsBase64Encoded = false
82+
};
6783
}
6884
}
6985

@@ -137,16 +153,7 @@ private static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2Response(string respo
137153
catch
138154
{
139155
// If deserialization fails, return Internal Server Error
140-
return new APIGatewayHttpApiV2ProxyResponse
141-
{
142-
StatusCode = 500,
143-
Body = "{\"message\":\"Internal Server Error\"}",
144-
Headers = new Dictionary<string, string>
145-
{
146-
{ "Content-Type", "application/json" }
147-
},
148-
IsBase64Encoded = false
149-
};
156+
return ToHttpApiV2ErrorResponse();
150157
}
151158
}
152159

@@ -177,4 +184,29 @@ private static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2Response(string respo
177184
};
178185
}
179186

187+
/// <summary>
188+
/// Creates a standard HTTP API v2 error response.
189+
/// </summary>
190+
/// <returns>An APIGatewayHttpApiV2ProxyResponse object representing the error response.</returns>
191+
/// <remarks>
192+
/// This method generates a standard error response for HTTP API v2:
193+
/// - StatusCode is set to 500 (Internal Server Error).
194+
/// - Body contains a JSON string with a generic error message.
195+
/// - Headers include a Content-Type set to application/json.
196+
/// - IsBase64Encoded is set to false.
197+
/// </remarks>
198+
public static APIGatewayHttpApiV2ProxyResponse ToHttpApiV2ErrorResponse()
199+
{
200+
return new APIGatewayHttpApiV2ProxyResponse
201+
{
202+
StatusCode = 500,
203+
Body = "{\"message\":\"Internal Server Error\"}",
204+
Headers = new Dictionary<string, string>
205+
{
206+
{ "Content-Type", "application/json" }
207+
},
208+
IsBase64Encoded = false
209+
};
210+
}
211+
180212
}

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,24 +99,35 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
9999
using var lambdaClient = CreateLambdaServiceClient(routeConfig, settings);
100100
var response = await lambdaClient.InvokeAsync(invokeRequest);
101101

102-
if (response.FunctionError != null)
102+
if (response.FunctionError == null) // response is successful
103103
{
104-
// TODO: Mimic API Gateway's behavior when Lambda function has an exception during invocation.
105-
context.Response.StatusCode = 500;
106-
return;
107-
}
108-
109-
// Convert API Gateway response object returned from Lambda to ASP.NET Core response.
110-
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
111-
{
112-
var lambdaResponse = response.ToApiGatewayHttpApiV2ProxyResponse();
113-
await lambdaResponse.ToHttpResponseAsync(context);
104+
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
105+
{
106+
var lambdaResponse = response.ToApiGatewayHttpApiV2ProxyResponse();
107+
await lambdaResponse.ToHttpResponseAsync(context);
108+
}
109+
else
110+
{
111+
var lambdaResponse = response.ToApiGatewayProxyResponse(settings.ApiGatewayEmulatorMode.Value);
112+
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
113+
}
114114
}
115115
else
116116
{
117-
var lambdaResponse = response.ToApiGatewayProxyResponse(settings.ApiGatewayEmulatorMode.Value);
118-
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
117+
// For function errors, api gateway just displays them as an internal server error, so we convert them to the correct error response here.
118+
119+
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
120+
{
121+
var lambdaResponse = InvokeResponseExtensions.ToHttpApiV2ErrorResponse();
122+
await lambdaResponse.ToHttpResponseAsync(context);
123+
}
124+
else
125+
{
126+
var lambdaResponse = InvokeResponseExtensions.ToApiGatewayErrorResponse(settings.ApiGatewayEmulatorMode.Value);
127+
await lambdaResponse.ToHttpResponseAsync(context, settings.ApiGatewayEmulatorMode.Value);
128+
}
119129
}
130+
120131
});
121132

122133
var runTask = app.RunAsync(cancellationToken);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Runtime.CompilerServices;
5+
6+
[assembly: InternalsVisibleTo("Amazon.Lambda.TestTool.UnitTests")]

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/LambdaRuntimeAPI.cs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
using System.Collections.Concurrent;
54
using System.Text;
65
using Amazon.Lambda.TestTool.Models;
76
using Microsoft.AspNetCore.Mvc;
@@ -15,7 +14,7 @@ public class LambdaRuntimeApi
1514

1615
private readonly IRuntimeApiDataStoreManager _runtimeApiDataStoreManager;
1716

18-
private LambdaRuntimeApi(WebApplication app)
17+
internal LambdaRuntimeApi(WebApplication app)
1918
{
2019
_runtimeApiDataStoreManager = app.Services.GetRequiredService<IRuntimeApiDataStoreManager>();
2120

@@ -63,16 +62,31 @@ public async Task PostEvent(HttpContext ctx, string functionName)
6362
if (isRequestResponseMode)
6463
{
6564
evnt.WaitForCompletion();
66-
var result = Results.Ok(evnt.Response);
67-
ctx.Response.StatusCode = 200;
6865

69-
if (!string.IsNullOrEmpty(evnt.Response))
66+
if (evnt.EventStatus == EventContainer.Status.Success)
7067
{
71-
var responseData = Encoding.UTF8.GetBytes(evnt.Response);
72-
ctx.Response.Headers.ContentType = "application/json";
73-
ctx.Response.Headers.ContentLength = responseData.Length;
74-
75-
await ctx.Response.Body.WriteAsync(responseData);
68+
var result = Results.Ok(evnt.Response);
69+
ctx.Response.StatusCode = 200;
70+
71+
if (!string.IsNullOrEmpty(evnt.Response))
72+
{
73+
var responseData = Encoding.UTF8.GetBytes(evnt.Response);
74+
ctx.Response.Headers.ContentType = "application/json";
75+
ctx.Response.Headers.ContentLength = responseData.Length;
76+
await ctx.Response.Body.WriteAsync(responseData);
77+
}
78+
}
79+
else
80+
{
81+
ctx.Response.StatusCode = 200;
82+
ctx.Response.Headers["X-Amz-Function-Error"] = evnt.ErrorType;
83+
if (!string.IsNullOrEmpty(evnt.ErrorResponse))
84+
{
85+
var errorData = Encoding.UTF8.GetBytes(evnt.ErrorResponse);
86+
ctx.Response.Headers.ContentType = "application/json";
87+
ctx.Response.Headers.ContentLength = errorData.Length;
88+
await ctx.Response.Body.WriteAsync(errorData);
89+
}
7690
}
7791
evnt.Dispose();
7892
}

0 commit comments

Comments
 (0)