Skip to content

Commit 5c1d3ef

Browse files
Api gateway response parsing (#1903)
1 parent 9366e67 commit 5c1d3ef

17 files changed

+1988
-3
lines changed

Tools/LambdaTestTool-v2/Amazon.Lambda.TestTool.sln

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.12.35527.113
5+
MinimumVisualStudioVersion = 10.0.40219.1
36
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
47
EndProject
58
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.TestTool", "src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj", "{97EE2E8A-D1F4-CB11-B664-B99B036E9F7B}"
@@ -8,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
811
EndProject
912
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.TestTool.UnitTests", "tests\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj", "{80A4F809-28B7-61EC-6539-DF3C7A0733FD}"
1013
EndProject
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.TestTool.IntegrationTests", "tests\Amazon.Lambda.TestTool.IntegrationTests\Amazon.Lambda.TestTool.IntegrationTests.csproj", "{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}"
15+
EndProject
1116
Global
1217
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1318
Debug|Any CPU = Debug|Any CPU
@@ -22,6 +27,13 @@ Global
2227
{80A4F809-28B7-61EC-6539-DF3C7A0733FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
2328
{80A4F809-28B7-61EC-6539-DF3C7A0733FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
2429
{80A4F809-28B7-61EC-6539-DF3C7A0733FD}.Release|Any CPU.Build.0 = Release|Any CPU
30+
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31+
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
32+
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
33+
{5C1B3E1C-DFEA-425B-8ED2-BB43BAECC3CB}.Release|Any CPU.Build.0 = Release|Any CPU
34+
EndGlobalSection
35+
GlobalSection(SolutionProperties) = preSolution
36+
HideSolutionNode = FALSE
2537
EndGlobalSection
2638
GlobalSection(NestedProjects) = preSolution
2739
{97EE2E8A-D1F4-CB11-B664-B99B036E9F7B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<Solution>
1+
<Solution>
22
<Folder Name="/src/">
3-
<Project Path="src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj" Type="Classic C#" />
3+
<Project Path="src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj" />
44
</Folder>
55
<Folder Name="/tests/">
6-
<Project Path="tests\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj" Type="Classic C#" />
6+
<Project Path="tests\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj" />
7+
<Project Path="tests\Amazon.Lambda.TestTool.IntegrationTests\Amazon.Lambda.TestTool.IntegrationTests.csproj" />
78
</Folder>
89
</Solution>

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<ItemGroup>
1818
<PackageReference Include="Spectre.Console" Version="0.49.1" />
1919
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
20+
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
2021
<PackageReference Include="Blazored.Modal" Version="7.3.1" />
2122
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
2223
</ItemGroup>
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using Amazon.Lambda.APIGatewayEvents;
5+
using Amazon.Lambda.TestTool.Models;
6+
using Microsoft.Extensions.Primitives;
7+
using System.Text;
8+
9+
namespace Amazon.Lambda.TestTool.Extensions;
10+
11+
/// <summary>
12+
/// Provides extension methods for converting API Gateway responses to <see cref="HttpResponse"/> objects.
13+
/// </summary>
14+
public static class ApiGatewayResponseExtensions
15+
{
16+
/// <summary>
17+
/// Converts an <see cref="APIGatewayProxyResponse"/> to an <see cref="HttpResponse"/>.
18+
/// </summary>
19+
/// <param name="apiResponse">The API Gateway proxy response to convert.</param>
20+
/// <param name="context">The <see cref="HttpContext"/> to use for the conversion.</param>
21+
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> to use for the conversion.</param>
22+
/// <returns>An <see cref="HttpResponse"/> representing the API Gateway response.</returns>
23+
public static void ToHttpResponse(this APIGatewayProxyResponse apiResponse, HttpContext httpContext, ApiGatewayEmulatorMode emulatorMode)
24+
{
25+
var response = httpContext.Response;
26+
response.Clear();
27+
28+
SetResponseHeaders(response, apiResponse.Headers, emulatorMode, apiResponse.MultiValueHeaders);
29+
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded);
30+
SetContentTypeAndStatusCodeV1(response, apiResponse.Headers, apiResponse.MultiValueHeaders, apiResponse.StatusCode, emulatorMode);
31+
}
32+
33+
/// <summary>
34+
/// Converts an <see cref="APIGatewayHttpApiV2ProxyResponse"/> to an <see cref="HttpResponse"/>.
35+
/// </summary>
36+
/// <param name="apiResponse">The API Gateway HTTP API v2 proxy response to convert.</param>
37+
/// <param name="context">The <see cref="HttpContext"/> to use for the conversion.</param>
38+
public static void ToHttpResponse(this APIGatewayHttpApiV2ProxyResponse apiResponse, HttpContext httpContext)
39+
{
40+
var response = httpContext.Response;
41+
response.Clear();
42+
43+
SetResponseHeaders(response, apiResponse.Headers, ApiGatewayEmulatorMode.HttpV2);
44+
SetResponseBody(response, apiResponse.Body, apiResponse.IsBase64Encoded);
45+
SetContentTypeAndStatusCodeV2(response, apiResponse.Headers, apiResponse.StatusCode);
46+
}
47+
48+
/// <summary>
49+
/// Sets the response headers on the <see cref="HttpResponse"/>, including default API Gateway headers based on the emulator mode.
50+
/// </summary>
51+
/// <param name="response">The <see cref="HttpResponse"/> to set headers on.</param>
52+
/// <param name="headers">The single-value headers to set.</param>
53+
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> determining which default headers to include.</param>
54+
/// <param name="multiValueHeaders">The multi-value headers to set.</param>
55+
private static void SetResponseHeaders(HttpResponse response, IDictionary<string, string>? headers, ApiGatewayEmulatorMode emulatorMode, IDictionary<string, IList<string>>? multiValueHeaders = null)
56+
{
57+
// Add default API Gateway headers
58+
var defaultHeaders = GetDefaultApiGatewayHeaders(emulatorMode);
59+
foreach (var header in defaultHeaders)
60+
{
61+
response.Headers[header.Key] = header.Value;
62+
}
63+
64+
if (multiValueHeaders != null)
65+
{
66+
foreach (var header in multiValueHeaders)
67+
{
68+
response.Headers[header.Key] = new StringValues(header.Value.ToArray());
69+
}
70+
}
71+
72+
if (headers != null)
73+
{
74+
foreach (var header in headers)
75+
{
76+
if (!response.Headers.ContainsKey(header.Key))
77+
{
78+
response.Headers[header.Key] = header.Value;
79+
}
80+
else
81+
{
82+
response.Headers.Append(header.Key, header.Value);
83+
}
84+
}
85+
}
86+
}
87+
88+
/// <summary>
89+
/// Generates default API Gateway headers based on the specified emulator mode.
90+
/// </summary>
91+
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> determining which headers to generate.</param>
92+
/// <returns>A dictionary of default headers appropriate for the specified emulator mode.</returns>
93+
private static Dictionary<string, string> GetDefaultApiGatewayHeaders(ApiGatewayEmulatorMode emulatorMode)
94+
{
95+
var headers = new Dictionary<string, string>
96+
{
97+
{ "Date", DateTime.UtcNow.ToString("r") },
98+
{ "Connection", "keep-alive" }
99+
};
100+
101+
switch (emulatorMode)
102+
{
103+
case ApiGatewayEmulatorMode.Rest:
104+
headers.Add("x-amzn-RequestId", Guid.NewGuid().ToString("D"));
105+
headers.Add("x-amz-apigw-id", GenerateRequestId());
106+
headers.Add("X-Amzn-Trace-Id", GenerateTraceId());
107+
break;
108+
case ApiGatewayEmulatorMode.HttpV1:
109+
case ApiGatewayEmulatorMode.HttpV2:
110+
headers.Add("Apigw-Requestid", GenerateRequestId());
111+
break;
112+
}
113+
114+
return headers;
115+
}
116+
117+
/// <summary>
118+
/// Generates a random X-Amzn-Trace-Id for REST API mode.
119+
/// </summary>
120+
/// <returns>A string representing a random X-Amzn-Trace-Id in the format used by API Gateway for REST APIs.</returns>
121+
/// <remarks>
122+
/// The generated trace ID includes:
123+
/// - A root ID with a timestamp and two partial GUIDs
124+
/// - A parent ID
125+
/// - A sampling decision (always set to 0 in this implementation)
126+
/// - A lineage identifier
127+
/// </remarks>
128+
private static string GenerateTraceId()
129+
{
130+
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString("x");
131+
var guid1 = Guid.NewGuid().ToString("N");
132+
var guid2 = Guid.NewGuid().ToString("N");
133+
return $"Root=1-{timestamp}-{guid1.Substring(0, 12)}{guid2.Substring(0, 12)};Parent={Guid.NewGuid().ToString("N").Substring(0, 16)};Sampled=0;Lineage=1:{Guid.NewGuid().ToString("N").Substring(0, 8)}:0";
134+
}
135+
136+
/// <summary>
137+
/// Generates a random API Gateway request ID for HTTP API v1 and v2.
138+
/// </summary>
139+
/// <returns>A string representing a random request ID in the format used by API Gateway for HTTP APIs.</returns>
140+
/// <remarks>
141+
/// The generated ID is a 15-character string consisting of lowercase letters and numbers, followed by an equals sign.
142+
/// </remarks>
143+
private static string GenerateRequestId()
144+
{
145+
return Guid.NewGuid().ToString("N").Substring(0, 8) + Guid.NewGuid().ToString("N").Substring(0, 7) + "=";
146+
}
147+
148+
/// <summary>
149+
/// Sets the response body on the <see cref="HttpResponse"/>.
150+
/// </summary>
151+
/// <param name="response">The <see cref="HttpResponse"/> to set the body on.</param>
152+
/// <param name="body">The body content.</param>
153+
/// <param name="isBase64Encoded">Whether the body is Base64 encoded.</param>
154+
private static void SetResponseBody(HttpResponse response, string? body, bool isBase64Encoded)
155+
{
156+
if (!string.IsNullOrEmpty(body))
157+
{
158+
byte[] bodyBytes;
159+
if (isBase64Encoded)
160+
{
161+
bodyBytes = Convert.FromBase64String(body);
162+
}
163+
else
164+
{
165+
bodyBytes = Encoding.UTF8.GetBytes(body);
166+
}
167+
168+
response.Body = new MemoryStream(bodyBytes);
169+
response.ContentLength = bodyBytes.Length;
170+
}
171+
}
172+
173+
/// <summary>
174+
/// Sets the content type and status code for API Gateway v1 responses.
175+
/// </summary>
176+
/// <param name="response">The <see cref="HttpResponse"/> to set the content type and status code on.</param>
177+
/// <param name="headers">The single-value headers.</param>
178+
/// <param name="multiValueHeaders">The multi-value headers.</param>
179+
/// <param name="statusCode">The status code to set.</param>
180+
/// <param name="emulatorMode">The <see cref="ApiGatewayEmulatorMode"/> being used.</param>
181+
private static void SetContentTypeAndStatusCodeV1(HttpResponse response, IDictionary<string, string>? headers, IDictionary<string, IList<string>>? multiValueHeaders, int statusCode, ApiGatewayEmulatorMode emulatorMode)
182+
{
183+
string? contentType = null;
184+
185+
if (headers != null && headers.TryGetValue("Content-Type", out var headerContentType))
186+
{
187+
contentType = headerContentType;
188+
}
189+
else if (multiValueHeaders != null && multiValueHeaders.TryGetValue("Content-Type", out var multiValueContentType))
190+
{
191+
contentType = multiValueContentType.FirstOrDefault();
192+
}
193+
194+
if (contentType != null)
195+
{
196+
response.ContentType = contentType;
197+
}
198+
else
199+
{
200+
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1)
201+
{
202+
response.ContentType = "text/plain; charset=utf-8";
203+
}
204+
else if (emulatorMode == ApiGatewayEmulatorMode.Rest)
205+
{
206+
response.ContentType = "application/json";
207+
}
208+
else
209+
{
210+
throw new ArgumentException("This function should only be called for ApiGatewayEmulatorMode.HttpV1 or ApiGatewayEmulatorMode.Rest");
211+
}
212+
}
213+
214+
if (statusCode != 0)
215+
{
216+
response.StatusCode = statusCode;
217+
}
218+
else
219+
{
220+
if (emulatorMode == ApiGatewayEmulatorMode.Rest) // rest api text for this message/error code is slightly different
221+
{
222+
response.StatusCode = 502;
223+
response.ContentType = "application/json";
224+
var errorBytes = Encoding.UTF8.GetBytes("{\"message\": \"Internal server error\"}");
225+
response.Body = new MemoryStream(errorBytes);
226+
response.ContentLength = errorBytes.Length;
227+
response.Headers["x-amzn-ErrorType"] = "InternalServerErrorException";
228+
}
229+
else
230+
{
231+
response.StatusCode = 500;
232+
response.ContentType = "application/json";
233+
var errorBytes = Encoding.UTF8.GetBytes("{\"message\":\"Internal Server Error\"}");
234+
response.Body = new MemoryStream(errorBytes);
235+
response.ContentLength = errorBytes.Length;
236+
}
237+
}
238+
}
239+
240+
/// <summary>
241+
/// Sets the content type and status code for API Gateway v2 responses.
242+
/// </summary>
243+
/// <param name="response">The <see cref="HttpResponse"/> to set the content type and status code on.</param>
244+
/// <param name="headers">The headers.</param>
245+
/// <param name="statusCode">The status code to set.</param>
246+
private static void SetContentTypeAndStatusCodeV2(HttpResponse response, IDictionary<string, string>? headers, int statusCode)
247+
{
248+
if (headers != null && headers.TryGetValue("Content-Type", out var contentType))
249+
{
250+
response.ContentType = contentType;
251+
}
252+
else
253+
{
254+
response.ContentType = "text/plain; charset=utf-8"; // api gateway v2 defaults to this content type if none is provided
255+
}
256+
257+
if (statusCode != 0)
258+
{
259+
response.StatusCode = statusCode;
260+
}
261+
else
262+
{
263+
response.StatusCode = 500;
264+
response.ContentType = "application/json";
265+
var errorBytes = Encoding.UTF8.GetBytes("{\"message\":\"Internal Server Error\"}");
266+
response.Body = new MemoryStream(errorBytes);
267+
response.ContentLength = errorBytes.Length;
268+
}
269+
}
270+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
14+
<PackageReference Include="AWSSDK.APIGateway" Version="3.7.401.7" />
15+
<PackageReference Include="AWSSDK.CloudFormation" Version="3.7.401.11" />
16+
<PackageReference Include="AWSSDK.IdentityManagement" Version="3.7.402" />
17+
<PackageReference Include="AWSSDK.ApiGatewayV2" Version="3.7.400.63" />
18+
<PackageReference Include="AWSSDK.Lambda" Version="3.7.408.1" />
19+
<PackageReference Include="coverlet.collector" Version="6.0.0" />
20+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
21+
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.0" />
22+
<PackageReference Include="xunit" Version="2.9.2" />
23+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
24+
</ItemGroup>
25+
26+
27+
<ItemGroup>
28+
<ProjectReference Include="..\..\src\Amazon.Lambda.TestTool\Amazon.Lambda.TestTool.csproj" />
29+
<ProjectReference Include="..\Amazon.Lambda.TestTool.UnitTests\Amazon.Lambda.TestTool.UnitTests.csproj" />
30+
</ItemGroup>
31+
32+
<ItemGroup>
33+
<EmbeddedResource Include="cloudformation-template-apigateway.yaml" />
34+
</ItemGroup>
35+
36+
37+
<ItemGroup>
38+
<Using Include="Xunit" />
39+
</ItemGroup>
40+
41+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.5.002.0
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.TestTool.IntegrationTests", "Amazon.Lambda.TestTool.IntegrationTests.csproj", "{94C7903E-A21A-43EC-BB04-C9DA404F1C02}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{94C7903E-A21A-43EC-BB04-C9DA404F1C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{94C7903E-A21A-43EC-BB04-C9DA404F1C02}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{94C7903E-A21A-43EC-BB04-C9DA404F1C02}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{94C7903E-A21A-43EC-BB04-C9DA404F1C02}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {429CE21F-1692-4C50-A9E6-299AB413D027}
24+
EndGlobalSection
25+
EndGlobal

0 commit comments

Comments
 (0)