Skip to content

Commit d96af57

Browse files
committed
PR comments
1 parent 9d38917 commit d96af57

File tree

10 files changed

+530
-374
lines changed

10 files changed

+530
-374
lines changed

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
namespace Amazon.Lambda.TestTool.Extensions;
55

6+
using System.Text;
67
using System.Web;
78
using Amazon.Lambda.APIGatewayEvents;
89
using Amazon.Lambda.TestTool.Models;
@@ -20,19 +21,20 @@ public static class HttpContextExtensions
2021
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
2122
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
2223
/// <returns>An <see cref="APIGatewayHttpApiV2ProxyRequest"/> object representing the translated request.</returns>
23-
public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
24+
public static async Task<APIGatewayHttpApiV2ProxyRequest> ToApiGatewayHttpV2Request(
2425
this HttpContext context,
2526
ApiGatewayRouteConfig apiGatewayRouteConfig)
2627
{
2728
var request = context.Request;
2829
var currentTime = DateTimeOffset.UtcNow;
29-
var body = HttpRequestUtility.ReadRequestBody(request);
30+
var body = await HttpRequestUtility.ReadRequestBody(request);
3031
var contentLength = HttpRequestUtility.CalculateContentLength(request, body);
3132

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

3435
// Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers are combined with commas and included in the headers field.
35-
var (_, allHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
36+
// 2.0 also lowercases all header keys
37+
var (_, allHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers, true);
3638
var headers = allHeaders.ToDictionary(
3739
kvp => kvp.Key,
3840
kvp => string.Join(", ", kvp.Value)
@@ -91,6 +93,7 @@ public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
9193
}
9294

9395
httpApiV2ProxyRequest.RawQueryString = string.Empty; // default is empty string
96+
9497
if (queryStringParameters.Any())
9598
{
9699
// this should be decoded value
@@ -123,35 +126,52 @@ public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
123126
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
124127
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
125128
/// <returns>An <see cref="APIGatewayProxyRequest"/> object representing the translated request.</returns>
126-
public static APIGatewayProxyRequest ToApiGatewayRequest(
129+
public static async Task<APIGatewayProxyRequest> ToApiGatewayRequest(
127130
this HttpContext context,
128-
ApiGatewayRouteConfig apiGatewayRouteConfig)
131+
ApiGatewayRouteConfig apiGatewayRouteConfig,
132+
ApiGatewayEmulatorMode emulatorMode)
129133
{
130134
var request = context.Request;
131-
var body = HttpRequestUtility.ReadRequestBody(request);
135+
var body = await HttpRequestUtility.ReadRequestBody(request);
132136
var contentLength = HttpRequestUtility.CalculateContentLength(request, body);
133137

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

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

139-
if (!headers.ContainsKey("content-length"))
143+
if (!headers.ContainsKey("content-length") && emulatorMode != ApiGatewayEmulatorMode.Rest) // rest doesnt set content-length by default
140144
{
141145
headers["content-length"] = contentLength.ToString();
142-
multiValueHeaders["content-length"] = new List<string> { contentLength.ToString() };
146+
multiValueHeaders["content-length"] = [contentLength.ToString()];
143147
}
144148

145149
if (!headers.ContainsKey("content-type"))
146150
{
147151
headers["content-type"] = "text/plain; charset=utf-8";
148-
multiValueHeaders["content-type"] = new List<string> { "text/plain; charset=utf-8" };
152+
multiValueHeaders["content-type"] = ["text/plain; charset=utf-8"];
153+
}
154+
155+
// This is the decoded value
156+
var path = request.Path.Value;
157+
158+
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1 || emulatorMode == ApiGatewayEmulatorMode.Rest) // rest and httpv1 uses the encoded value for path an
159+
{
160+
path = request.Path.ToUriComponent();
161+
}
162+
163+
if (emulatorMode == ApiGatewayEmulatorMode.Rest) // rest uses encoded value for the path params
164+
{
165+
var encodedPathParameters = pathParameters.ToDictionary(
166+
kvp => kvp.Key,
167+
kvp => Uri.EscapeUriString(kvp.Value)); // intentionally using EscapeURiString over EscapeDataString since EscapeURiString correctly handles reserved characters :/?#[]@!$&'()*+,;= in this case
168+
pathParameters = encodedPathParameters;
149169
}
150170

151171
var proxyRequest = new APIGatewayProxyRequest
152172
{
153173
Resource = apiGatewayRouteConfig.Path,
154-
Path = request.Path.Value,
174+
Path = path,
155175
HttpMethod = request.Method,
156176
Body = body,
157177
IsBase64Encoded = false
@@ -181,13 +201,11 @@ public static APIGatewayProxyRequest ToApiGatewayRequest(
181201

182202
if (pathParameters.Any())
183203
{
184-
// this should be decoded value
185204
proxyRequest.PathParameters = pathParameters;
186205
}
187206

188207
if (HttpRequestUtility.IsBinaryContent(request.ContentType))
189208
{
190-
// we already converted it when we read the body so we dont need to re-convert it
191209
proxyRequest.IsBase64Encoded = true;
192210
}
193211

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using System.Text;
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Text;
25

36
namespace Amazon.Lambda.TestTool.Utilities;
47

@@ -32,7 +35,7 @@ public static bool IsBinaryContent(string? contentType)
3235
/// </summary>
3336
/// <param name="request">The HTTP request.</param>
3437
/// <returns>The body of the request as a string, or null if the body is empty.</returns>
35-
public static string? ReadRequestBody(HttpRequest request)
38+
public static async Task<string?> ReadRequestBody(HttpRequest request)
3639
{
3740
if (request.ContentLength == 0 || request.Body == null || !request.Body.CanRead)
3841
{
@@ -46,7 +49,7 @@ public static bool IsBinaryContent(string? contentType)
4649

4750
using (var memoryStream = new MemoryStream())
4851
{
49-
request.Body.CopyTo(memoryStream);
52+
await request.Body.CopyToAsync(memoryStream);
5053

5154
// If the stream is empty, return null
5255
if (memoryStream.Length == 0)
@@ -67,7 +70,7 @@ public static bool IsBinaryContent(string? contentType)
6770
// For text data, read as string
6871
using (var reader = new StreamReader(memoryStream))
6972
{
70-
string content = reader.ReadToEnd();
73+
string content = await reader.ReadToEndAsync();
7174
return string.IsNullOrWhiteSpace(content) ? null : content;
7275
}
7376
}
@@ -80,6 +83,7 @@ public static bool IsBinaryContent(string? contentType)
8083
/// Extracts headers from the request, separating them into single-value and multi-value dictionaries.
8184
/// </summary>
8285
/// <param name="headers">The request headers.</param>
86+
/// <param name="lowerCaseKeyName">Whether to lowercase the key name or not.</param>
8387
/// <returns>A tuple containing single-value and multi-value header dictionaries.</returns>
8488
/// <example>
8589
/// For headers:
@@ -91,15 +95,16 @@ public static bool IsBinaryContent(string? contentType)
9195
/// singleValueHeaders: { "Accept": "application/xhtml+xml", "X-Custom-Header": "value1" }
9296
/// multiValueHeaders: { "Accept": ["text/html", "application/xhtml+xml"], "X-Custom-Header": ["value1"] }
9397
/// </example>
94-
public static (IDictionary<string, string>, IDictionary<string, IList<string>>) ExtractHeaders(IHeaderDictionary headers)
98+
public static (IDictionary<string, string>, IDictionary<string, IList<string>>) ExtractHeaders(IHeaderDictionary headers, bool lowerCaseKeyName = false)
9599
{
96-
var singleValueHeaders = new Dictionary<string, string>();
97-
var multiValueHeaders = new Dictionary<string, IList<string>>();
100+
var singleValueHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
101+
var multiValueHeaders = new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
98102

99103
foreach (var header in headers)
100104
{
101-
singleValueHeaders[header.Key.ToLower()] = header.Value.Last() ?? "";
102-
multiValueHeaders[header.Key.ToLower()] = [.. header.Value];
105+
var key = lowerCaseKeyName ? header.Key.ToLower() : header.Key;
106+
singleValueHeaders[key] = header.Value.Last() ?? "";
107+
multiValueHeaders[key] = [.. header.Value];
103108
}
104109

105110
return (singleValueHeaders, multiValueHeaders);
@@ -139,7 +144,7 @@ public static (IDictionary<string, string>, IDictionary<string, IList<string>>)
139144
/// The generated ID is a 145character string consisting of lowercase letters and numbers, followed by an equals sign.
140145
public static string GenerateRequestId()
141146
{
142-
return Guid.NewGuid().ToString("N").Substring(0, 8) + Guid.NewGuid().ToString("N").Substring(0, 7) + "=";
147+
return $"{Guid.NewGuid().ToString("N").Substring(0, 8)}{Guid.NewGuid().ToString("N").Substring(0, 7)}=";
143148
}
144149

145150
/// <summary>
@@ -161,7 +166,7 @@ public static string GenerateTraceId()
161166
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";
162167
}
163168

164-
public static long CalculateContentLength(HttpRequest request, string body)
169+
public static long CalculateContentLength(HttpRequest request, string? body)
165170
{
166171
if (!string.IsNullOrEmpty(body))
167172
{
Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
namespace Amazon.Lambda.TestTool.Utilities;
1+
namespace Amazon.Lambda.TestTool.Utilities;
22

3+
using System.Text.RegularExpressions;
34
using Microsoft.AspNetCore.Routing.Template;
45

56
/// <summary>
67
/// Provides utility methods for working with route templates and extracting path parameters.
78
/// </summary>
89
public static class RouteTemplateUtility
910
{
11+
private const string TemporaryPrefix = "__aws_param__";
12+
1013
/// <summary>
1114
/// Extracts path parameters from an actual path based on a route template.
1215
/// </summary>
@@ -24,43 +27,88 @@ public static class RouteTemplateUtility
2427
/// </example>
2528
public static Dictionary<string, string> ExtractPathParameters(string routeTemplate, string actualPath)
2629
{
30+
// Preprocess the route template to convert from .net style format to aws
31+
routeTemplate = PreprocessRouteTemplate(routeTemplate);
32+
2733
var template = TemplateParser.Parse(routeTemplate);
28-
var matcher = new TemplateMatcher(template, GetDefaults(template));
34+
var matcher = new TemplateMatcher(template, new RouteValueDictionary());
2935
var routeValues = new RouteValueDictionary();
3036

3137
if (matcher.TryMatch(actualPath, routeValues))
3238
{
33-
return routeValues.ToDictionary(rv => rv.Key, rv => rv.Value?.ToString() ?? string.Empty);
39+
var result = new Dictionary<string, string>();
40+
41+
foreach (var param in template.Parameters)
42+
{
43+
if (routeValues.TryGetValue(param.Name, out var value))
44+
{
45+
var stringValue = value?.ToString() ?? string.Empty;
46+
47+
// For catch-all parameters, remove the leading slash if present
48+
if (param.IsCatchAll)
49+
{
50+
stringValue = stringValue.TrimStart('/');
51+
}
52+
53+
// Restore original parameter name
54+
var originalParamName = RestoreOriginalParamName(param.Name);
55+
result[originalParamName] = stringValue;
56+
}
57+
}
58+
59+
return result;
3460
}
3561

3662
return new Dictionary<string, string>();
3763
}
3864

3965
/// <summary>
40-
/// Gets the default values for parameters in a parsed route template.
66+
/// Preprocesses a route template to make it compatible with ASP.NET Core's TemplateMatcher.
4167
/// </summary>
42-
/// <param name="parsedTemplate">The parsed route template.</param>
43-
/// <returns>A dictionary of default values for the template parameters.</returns>
44-
/// <example>
45-
/// Using this method:
46-
/// <code>
47-
/// var template = TemplateParser.Parse("/api/{version=v1}/users/{id}");
48-
/// var defaults = RouteTemplateUtility.GetDefaults(template);
49-
/// // defaults will contain: { {"version", "v1"} }
50-
/// </code>
51-
/// </example>
52-
public static RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate)
68+
/// <param name="template">The original route template, potentially in AWS API Gateway format.</param>
69+
/// <returns>A preprocessed route template compatible with ASP.NET Core's TemplateMatcher.</returns>
70+
/// <remarks>
71+
/// This method performs two main transformations:
72+
/// 1. Converts AWS-style {proxy+} to ASP.NET Core style {*proxy}
73+
/// 2. Handles AWS ignoring constraignts by temporarily renaming parameters
74+
/// (e.g., {abc:int} becomes {__aws_param__abc__int})
75+
/// </remarks>
76+
private static string PreprocessRouteTemplate(string template)
5377
{
54-
var result = new RouteValueDictionary();
78+
// Convert AWS-style {proxy+} to ASP.NET Core style {*proxy}
79+
template = Regex.Replace(template, @"\{(\w+)\+\}", "{*$1}");
80+
81+
// Handle AWS-style "constraints" by replacing them with temporary parameter names
82+
return Regex.Replace(template, @"\{([^}]+):([^}]+)\}", match =>
83+
{
84+
var paramName = match.Groups[1].Value;
85+
var constraint = match.Groups[2].Value;
86+
87+
// There is a low chance that one of the parameters being used actually follows the syntax of {TemporaryPrefix}{paramName}__{constraint}.
88+
// But i dont think its signifigant enough to worry about.
89+
return $"{{{TemporaryPrefix}{paramName}__{constraint}}}";
90+
});
91+
}
5592

56-
foreach (var parameter in parsedTemplate.Parameters)
93+
/// <summary>
94+
/// Restores the original parameter name after processing by TemplateMatcher.
95+
/// </summary>
96+
/// <param name="processedName">The parameter name after processing and matching.</param>
97+
/// <returns>The original parameter name.</returns>
98+
/// <remarks>
99+
/// This method reverses the transformation done in PreprocessRouteTemplate.
100+
/// For example, "__aws_param__abc__int" would be restored to "abc:int".
101+
/// </remarks>
102+
private static string RestoreOriginalParamName(string processedName)
103+
{
104+
if (processedName.StartsWith(TemporaryPrefix))
57105
{
58-
if (parameter.DefaultValue != null)
106+
var parts = processedName.Substring(TemporaryPrefix.Length).Split("__", 2);
107+
if (parts.Length == 2)
59108
{
60-
if (parameter.Name != null) result.Add(parameter.Name, parameter.DefaultValue);
109+
return $"{parts[0]}:{parts[1]}";
61110
}
62111
}
63-
64-
return result;
112+
return processedName;
65113
}
66114
}

0 commit comments

Comments
 (0)